[core] Move storage to SQLite database (#30)

fixes #29
This commit is contained in:
Knut Ahlers 2022-09-10 13:39:07 +02:00 committed by GitHub
parent c1a7221b06
commit a7533cbd8b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
72 changed files with 2127 additions and 921 deletions

2
.gitignore vendored
View file

@ -6,5 +6,7 @@ editor/app.js
editor/bundle.*
.env
node_modules
storage.db
storage.db-journal
storage.json.gz
twitch-bot

View file

@ -23,16 +23,56 @@ 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-encryption-pass string Passphrase to encrypt secrets inside storage (defaults to twitch-client:twitch-client-secret)
--storage-file string Where to store the data (default "./storage.json.gz")
--twitch-client string Client ID to act as
--twitch-client-secret string Secret for the Client ID
--twitch-token string OAuth token valid for client (fallback if no token was set in interface)
-v, --validate-config Loads the config, logs any errors and quits with status 0 on success
--version Prints current version and exits
# twitch-bot help
Supported sub-commands are:
actor-docs Generate markdown documentation for available actors
api-token <name> <scope...> Generate an api-token to be entered into the config
migrate-v2 <old file> Migrate old (*.json.gz) storage file into new database
help Prints this help message
```
## 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.
**Before starting the migration make sure to fully stop the bot!**
This section assumes you were starting your bot the following way:
```console
# twitch-bot --storage-file storage.json.gz --twitch-client <clientid> --twitch-client-secret <secret>
```
To execute the migration we need to provide the same `storage-encryption-pass` or `twitch-client` / `twitch-client-secret` combination if no `storage-encryption-pass` was used.
```console
# twitch-bot --storage-database storage.db --twitch-client <clientid> --twitch-client-secret <secret> migrate-v2 storage.json.gz
WARN[0000] No storage encryption passphrase was set, falling back to client-id:client-secret
WARN[0000] Module registered unhandled query-param type module=status type=integer
WARN[0000] Overlays dir not specified, no dir or non existent dir=
INFO[0000] Starting migration... module=variables
INFO[0000] Starting migration... module=mod_punish
INFO[0000] Starting migration... module=mod_overlays
INFO[0000] Starting migration... module=mod_quotedb
INFO[0000] Starting migration... module=core
INFO[0000] Starting migration... module=counter
INFO[0000] Starting migration... module=permissions
INFO[0000] Starting migration... module=timers
INFO[0000] v2 storage file was migrated
```
If you see the `v2 storage file was migrated` message the contents of your old storage file were migrated to the new `storage-database`. The old file is not modified in this step.
Afterwards your need to adjust the start parameters of the bot:
```console
# twitch-bot --storage-database storage.db --twitch-client <clientid> --twitch-client-secret <secret>
```

View file

@ -10,8 +10,8 @@ import (
"github.com/go-irc/irc"
"github.com/pkg/errors"
"github.com/Luzifer/twitch-bot/pkg/twitch"
"github.com/Luzifer/twitch-bot/plugins"
"github.com/Luzifer/twitch-bot/twitch"
)
func init() {

View file

@ -85,7 +85,7 @@ func handleMessage(c *irc.Client, m *irc.Message, event *string, eventData *plug
// Lock command
if !preventCooldown {
r.SetCooldown(timerStore, m, eventData)
r.SetCooldown(timerService, m, eventData)
}
}
}

16
auth.go
View file

@ -11,8 +11,8 @@ import (
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/Luzifer/twitch-bot/pkg/twitch"
"github.com/Luzifer/twitch-bot/plugins"
"github.com/Luzifer/twitch-bot/twitch"
)
var instanceState = uuid.Must(uuid.NewV4()).String()
@ -85,18 +85,14 @@ func handleAuthUpdateBotToken(w http.ResponseWriter, r *http.Request) {
return
}
if err = store.UpdateBotToken(rData.AccessToken, rData.RefreshToken); err != nil {
if err = accessService.SetBotTwitchCredentials(rData.AccessToken, rData.RefreshToken); err != nil {
http.Error(w, errors.Wrap(err, "storing access token").Error(), http.StatusInternalServerError)
return
}
twitchClient.UpdateToken(rData.AccessToken, rData.RefreshToken)
if err = store.SetExtendedPermissions(botUser, storageExtendedPermission{
AccessToken: rData.AccessToken,
RefreshToken: rData.RefreshToken,
Scopes: rData.Scope,
}, true); err != nil {
if err = accessService.SetExtendedTwitchCredentials(botUser, rData.AccessToken, rData.RefreshToken, rData.Scope); err != nil {
http.Error(w, errors.Wrap(err, "storing access scopes").Error(), http.StatusInternalServerError)
return
}
@ -145,11 +141,7 @@ func handleAuthUpdateChannelGrant(w http.ResponseWriter, r *http.Request) {
return
}
if err = store.SetExtendedPermissions(grantUser, storageExtendedPermission{
AccessToken: rData.AccessToken,
RefreshToken: rData.RefreshToken,
Scopes: rData.Scope,
}, false); err != nil {
if err = accessService.SetExtendedTwitchCredentials(grantUser, rData.AccessToken, rData.RefreshToken, rData.Scope); err != nil {
http.Error(w, errors.Wrap(err, "storing access token").Error(), http.StatusInternalServerError)
return
}

View file

@ -6,7 +6,7 @@ import (
"github.com/pkg/errors"
"github.com/Luzifer/go_helpers/v2/str"
"github.com/Luzifer/twitch-bot/twitch"
"github.com/Luzifer/twitch-bot/pkg/twitch"
)
func getAuthorizationFromRequest(r *http.Request) (string, *twitch.Client, error) {

View file

@ -6,7 +6,7 @@ import (
"github.com/go-irc/irc"
"github.com/Luzifer/twitch-bot/twitch"
"github.com/Luzifer/twitch-bot/pkg/twitch"
)
type (

View file

@ -257,7 +257,7 @@ func (c configFile) GetMatchingRules(m *irc.Message, event *string, eventData *p
var out []*plugins.Rule
for _, r := range c.Rules {
if r.Matches(m, event, timerStore, formatMessage, twitchClient, eventData) {
if r.Matches(m, event, timerService, formatMessage, twitchClient, eventData) {
out = append(out, r)
}
}

View file

@ -11,8 +11,8 @@ import (
"github.com/gorilla/websocket"
"github.com/pkg/errors"
"github.com/Luzifer/twitch-bot/pkg/twitch"
"github.com/Luzifer/twitch-bot/plugins"
"github.com/Luzifer/twitch-bot/twitch"
)
const websocketPingInterval = 30 * time.Second

View file

@ -183,10 +183,16 @@ func configEditorHandleGeneralDeleteAuthToken(w http.ResponseWriter, r *http.Req
}
func configEditorHandleGeneralGet(w http.ResponseWriter, r *http.Request) {
elevated := make(map[string]bool)
var (
elevated = make(map[string]bool)
err error
)
for _, ch := range config.Channels {
elevated[ch] = store.UserHasGrantedScopes(ch, channelDefaultScopes...) && store.UserHasExtendedAuth(ch)
if elevated[ch], err = accessService.HasPermissionsForChannel(ch, channelDefaultScopes...); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
var uName *string

View file

@ -99,12 +99,4 @@ func init() {
tplFuncs.Register("toLower", plugins.GenericTemplateFunctionGetter(strings.ToLower))
tplFuncs.Register("toUpper", plugins.GenericTemplateFunctionGetter(strings.ToUpper))
tplFuncs.Register("variable", plugins.GenericTemplateFunctionGetter(func(name string, defVal ...string) string {
value := store.GetVariable(name)
if value == "" && len(defVal) > 0 {
return defVal[0]
}
return value
}))
}

View file

@ -1,27 +0,0 @@
package main
import (
"strings"
"github.com/go-irc/irc"
"github.com/pkg/errors"
"github.com/Luzifer/twitch-bot/plugins"
)
func init() {
tplFuncs.Register("channelCounter", func(m *irc.Message, r *plugins.Rule, fields *plugins.FieldCollection) interface{} {
return func(name string) (string, error) {
channel, err := fields.String("channel")
if err != nil {
return "", errors.Wrap(err, "channel not available")
}
return strings.Join([]string{channel, name}, ":"), nil
}
})
tplFuncs.Register("counterValue", plugins.GenericTemplateFunctionGetter(func(name string, _ ...string) int64 {
return store.GetCounterValue(name)
}))
}

11
go.mod
View file

@ -7,11 +7,13 @@ require (
github.com/Luzifer/go_helpers/v2 v2.12.2
github.com/Luzifer/korvike/functions v0.6.1
github.com/Luzifer/rconfig/v2 v2.3.0
github.com/glebarez/go-sqlite v1.18.1
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/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
@ -26,6 +28,7 @@ require (
github.com/cenkalti/backoff/v3 v3.2.2 // indirect
github.com/emirpasic/gods v1.12.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
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
@ -37,20 +40,26 @@ require (
github.com/hashicorp/vault/sdk v0.2.1 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.4.1 // indirect
github.com/pierrec/lz4 v2.6.1+incompatible // 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
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/sys v0.0.0-20210124154548-22da62e12c0c // indirect
golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64 // indirect
golang.org/x/text v0.3.6 // 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.0-20210331031555-b37d688a7fb0 // 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
)

49
go.sum
View file

@ -60,6 +60,7 @@ github.com/docker/docker v1.4.2-0.20200319182547-c7ad2b866182/go.mod h1:eEKB0N0r
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@ -72,6 +73,8 @@ github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI
github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
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/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-irc/irc v2.1.0+incompatible h1:pg7pMVq5OYQbqTxceByD/EN8VIsba7DtKn49rsCnG8Y=
@ -84,6 +87,7 @@ github.com/go-ldap/ldap/v3 v3.1.10/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9p
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-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
@ -118,8 +122,11 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
@ -191,9 +198,12 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
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=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY=
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
@ -209,6 +219,7 @@ github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
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/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
@ -216,6 +227,10 @@ github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx
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 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
@ -286,6 +301,8 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/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/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
@ -400,6 +417,10 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
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-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64 h1:D1v9ucDTYBtbz5vNuBbAhIMAGhQhJ6Ym5ah3maMVNX4=
golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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=
@ -425,6 +446,7 @@ golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgw
golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/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-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-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -494,3 +516,30 @@ gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81
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=
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/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/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/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/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw=
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8=

View file

@ -1,6 +1,3 @@
package main
var (
ptrBoolFalse = func(v bool) *bool { return &v }(false)
ptrStringEmpty = func(v string) *string { return &v }("")
)
var ptrBoolFalse = func(v bool) *bool { return &v }(false)

View file

@ -1,21 +1,38 @@
package main
package counter
import (
"fmt"
"net/http"
"strconv"
"strings"
"github.com/go-irc/irc"
"github.com/gorilla/mux"
"github.com/pkg/errors"
"github.com/Luzifer/twitch-bot/pkg/database"
"github.com/Luzifer/twitch-bot/plugins"
)
func init() {
registerAction("counter", func() plugins.Actor { return &ActorCounter{} })
var (
db database.Connector
formatMessage plugins.MsgFormatter
registerActorDocumentation(plugins.ActionDocumentation{
ptrStringEmpty = func(s string) *string { return &s }("")
)
//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 {
return errors.Wrap(err, "applying schema migration")
}
formatMessage = args.FormatMessage
args.RegisterActor("counter", func() plugins.Actor { return &ActorCounter{} })
args.RegisterActorDocumentation(plugins.ActionDocumentation{
Description: "Update counter values",
Name: "Modify Counter",
Type: "counter",
@ -51,7 +68,7 @@ func init() {
},
})
registerRoute(plugins.HTTPRouteRegistrationArgs{
args.RegisterAPIRoute(plugins.HTTPRouteRegistrationArgs{
Description: "Returns the (formatted) value as a plain string",
HandlerFunc: routeActorCounterGetValue,
Method: http.MethodGet,
@ -75,7 +92,7 @@ func init() {
},
})
registerRoute(plugins.HTTPRouteRegistrationArgs{
args.RegisterAPIRoute(plugins.HTTPRouteRegistrationArgs{
Description: "Updates the value of the counter",
HandlerFunc: routeActorCounterSetValue,
Method: http.MethodPatch,
@ -104,6 +121,23 @@ func init() {
},
},
})
args.RegisterTemplateFunction("channelCounter", func(m *irc.Message, r *plugins.Rule, fields *plugins.FieldCollection) interface{} {
return func(name string) (string, error) {
channel, err := fields.String("channel")
if err != nil {
return "", errors.Wrap(err, "channel not available")
}
return strings.Join([]string{channel, name}, ":"), nil
}
})
args.RegisterTemplateFunction("counterValue", plugins.GenericTemplateFunctionGetter(func(name string, _ ...string) (int64, error) {
return getCounterValue(name)
}))
return nil
}
type ActorCounter struct{}
@ -126,7 +160,7 @@ func (a ActorCounter) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, ev
}
return false, errors.Wrap(
store.UpdateCounter(counterName, counterValue, true),
updateCounter(counterName, counterValue, true),
"set counter",
)
}
@ -145,7 +179,7 @@ func (a ActorCounter) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, ev
}
return false, errors.Wrap(
store.UpdateCounter(counterName, counterStep, false),
updateCounter(counterName, counterStep, false),
"update counter",
)
}
@ -167,8 +201,14 @@ func routeActorCounterGetValue(w http.ResponseWriter, r *http.Request) {
template = "%d"
}
cv, err := getCounterValue(mux.Vars(r)["name"])
if err != nil {
http.Error(w, errors.Wrap(err, "getting value").Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text-plain")
fmt.Fprintf(w, template, store.GetCounterValue(mux.Vars(r)["name"]))
fmt.Fprintf(w, template, cv)
}
func routeActorCounterSetValue(w http.ResponseWriter, r *http.Request) {
@ -183,7 +223,7 @@ func routeActorCounterSetValue(w http.ResponseWriter, r *http.Request) {
return
}
if err = store.UpdateCounter(mux.Vars(r)["name"], value, absolute); err != nil {
if err = updateCounter(mux.Vars(r)["name"], value, absolute); err != nil {
http.Error(w, errors.Wrap(err, "updating value").Error(), http.StatusInternalServerError)
return
}

View file

@ -0,0 +1,55 @@
package counter
import (
"database/sql"
"embed"
"github.com/pkg/errors"
)
//go:embed schema/**
var schema embed.FS
func getCounterValue(counter string) (int64, error) {
row := db.DB().QueryRow(
`SELECT value
FROM counters
WHERE name = $1`,
counter,
)
var cv int64
err := row.Scan(&cv)
switch {
case err == nil:
return cv, nil
case errors.Is(err, sql.ErrNoRows):
return 0, nil
default:
return 0, errors.Wrap(err, "querying counter")
}
}
func updateCounter(counter string, value int64, absolute bool) error {
if !absolute {
cv, err := getCounterValue(counter)
if err != nil {
return errors.Wrap(err, "getting previous value")
}
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(err, "storing counter value")
}

View file

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

View file

@ -7,8 +7,8 @@ import (
"github.com/go-irc/irc"
"github.com/pkg/errors"
"github.com/Luzifer/twitch-bot/pkg/twitch"
"github.com/Luzifer/twitch-bot/plugins"
"github.com/Luzifer/twitch-bot/twitch"
)
const actorName = "modchannel"

View file

@ -12,8 +12,8 @@ import (
log "github.com/sirupsen/logrus"
"github.com/Luzifer/go_helpers/v2/str"
"github.com/Luzifer/twitch-bot/pkg/twitch"
"github.com/Luzifer/twitch-bot/plugins"
"github.com/Luzifer/twitch-bot/twitch"
)
const (

View file

@ -1,16 +1,15 @@
package punish
import (
"encoding/json"
"math"
"strconv"
"strings"
"sync"
"time"
"github.com/go-irc/irc"
"github.com/pkg/errors"
"github.com/Luzifer/twitch-bot/pkg/database"
"github.com/Luzifer/twitch-bot/plugins"
)
@ -23,16 +22,19 @@ const (
)
var (
db database.Connector
formatMessage plugins.MsgFormatter
ptrDefaultCooldown = func(v time.Duration) *time.Duration { return &v }(oneWeek)
ptrStringEmpty = func(v string) *string { return &v }("")
store plugins.StorageManager
storedObject = newStorage()
)
func Register(args plugins.RegistrationArguments) error {
db = args.GetDatabaseConnector()
if err := db.Migrate("punish", database.NewEmbedFSMigrator(schema, "schema")); err != nil {
return errors.Wrap(err, "applying schema migration")
}
formatMessage = args.FormatMessage
store = args.GetStorageManager()
args.RegisterActor(actorNamePunish, func() plugins.Actor { return &actorPunish{} })
args.RegisterActor(actorNameResetPunish, func() plugins.Actor { return &actorResetPunish{} })
@ -118,10 +120,7 @@ func Register(args plugins.RegistrationArguments) error {
},
})
return errors.Wrap(
store.GetModuleStore(moduleUUID, storedObject),
"loading module storage",
)
return nil
}
type (
@ -133,12 +132,6 @@ type (
Executed time.Time `json:"executed"`
Cooldown time.Duration `json:"cooldown"`
}
storage struct {
ActiveLevels map[string]*levelConfig `json:"active_levels"`
lock sync.Mutex
}
)
// Punish
@ -160,7 +153,10 @@ func (a actorPunish) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eve
return false, errors.Wrap(err, "preparing user")
}
lvl := storedObject.GetPunishment(plugins.DeriveChannel(m, eventData), user, uuid)
lvl, err := getPunishment(plugins.DeriveChannel(m, eventData), user, uuid)
if err != nil {
return false, errors.Wrap(err, "getting stored punishment")
}
nLvl := int(math.Min(float64(len(levels)-1), float64(lvl.LastLevel+1)))
var cmd []string
@ -207,7 +203,7 @@ func (a actorPunish) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eve
lvl.LastLevel = nLvl
return false, errors.Wrap(
store.SetModuleStore(moduleUUID, storedObject),
setPunishment(plugins.DeriveChannel(m, eventData), user, uuid, lvl),
"storing punishment level",
)
}
@ -239,10 +235,8 @@ func (a actorResetPunish) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule
return false, errors.Wrap(err, "preparing user")
}
storedObject.ResetLevel(plugins.DeriveChannel(m, eventData), user, uuid)
return false, errors.Wrap(
store.SetModuleStore(moduleUUID, storedObject),
deletePunishment(plugins.DeriveChannel(m, eventData), user, uuid),
"resetting punishment level",
)
}
@ -257,91 +251,3 @@ func (a actorResetPunish) Validate(attrs *plugins.FieldCollection) (err error) {
return nil
}
// Storage
func newStorage() *storage {
return &storage{
ActiveLevels: make(map[string]*levelConfig),
}
}
func (s *storage) GetPunishment(channel, user, uuid string) *levelConfig {
s.lock.Lock()
defer s.lock.Unlock()
// Ensure old state is cleared
s.calculateCooldowns()
var (
id = s.getCacheKey(channel, user, uuid)
lvl = s.ActiveLevels[id]
)
if lvl == nil {
// Initialize a non-triggered state
lvl = &levelConfig{LastLevel: -1}
s.ActiveLevels[id] = lvl
}
return lvl
}
func (s *storage) ResetLevel(channel, user, uuid string) {
s.lock.Lock()
defer s.lock.Unlock()
delete(s.ActiveLevels, s.getCacheKey(channel, user, uuid))
}
func (s *storage) getCacheKey(channel, user, uuid string) string {
return strings.Join([]string{channel, user, uuid}, "::")
}
func (s *storage) calculateCooldowns() {
// This MUST NOT be locked, the lock MUST be set by calling method
var clear []string
for id, lvl := range s.ActiveLevels {
for {
cooldownTime := lvl.Executed.Add(lvl.Cooldown)
if cooldownTime.After(time.Now()) {
break
}
lvl.Executed = cooldownTime
lvl.LastLevel--
}
// Level 0 is the first punishment level, so only remove if it drops below 0
if lvl.LastLevel < 0 {
clear = append(clear, id)
}
}
for _, id := range clear {
delete(s.ActiveLevels, id)
}
}
// Implement marshaller interfaces
func (s *storage) MarshalStoredObject() ([]byte, error) {
s.lock.Lock()
defer s.lock.Unlock()
s.calculateCooldowns()
return json.Marshal(s)
}
func (s *storage) UnmarshalStoredObject(data []byte) error {
if data == nil {
// No data set yet, don't try to unmarshal
return nil
}
s.lock.Lock()
defer s.lock.Unlock()
return json.Unmarshal(data, s)
}

View file

@ -0,0 +1,142 @@
package punish
import (
"database/sql"
"embed"
"strings"
"time"
"github.com/pkg/errors"
)
//go:embed schema/**
var schema embed.FS
func calculateCurrentPunishments() error {
rows, err := db.DB().Query(
`SELECT key, last_level, executed, cooldown
FROM punish_levels;`,
)
if err != nil {
return errors.Wrap(err, "querying punish_levels")
}
for rows.Next() {
if err = rows.Err(); err != nil {
return errors.Wrap(err, "advancing rows")
}
var (
key string
lastLevel, executed, cooldown int64
actUpdate bool
)
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()) {
break
}
lvl.Executed = cooldownTime
lvl.LastLevel--
actUpdate = true
}
// 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 {
return errors.Wrap(err, "cleaning up expired punishment")
}
continue
}
if actUpdate {
if err = setPunishmentForKey(key, lvl); err != nil {
return errors.Wrap(err, "updating punishment")
}
}
}
return errors.Wrap(rows.Err(), "finishing rows processing")
}
func deletePunishment(channel, user, uuid string) error {
return deletePunishmentForKey(getDBKey(channel, user, uuid))
}
func deletePunishmentForKey(key string) error {
_, err := db.DB().Exec(
`DELETE FROM punish_levels
WHERE key = $1;`,
key,
)
return errors.Wrap(err, "deleting punishment info")
}
func getPunishment(channel, user, uuid string) (*levelConfig, error) {
if err := calculateCurrentPunishments(); 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),
)
lc := &levelConfig{LastLevel: -1}
var lastLevel, executed, cooldown int64
err := row.Scan(&lastLevel, &executed, &cooldown)
switch {
case err == nil:
lc.LastLevel = int(lastLevel)
lc.Cooldown = time.Duration(cooldown)
lc.Executed = time.Unix(executed, 0)
return lc, nil
case errors.Is(err, sql.ErrNoRows):
return lc, nil
default:
return nil, errors.Wrap(err, "getting punishment from database")
}
}
func setPunishment(channel, user, uuid string, lc *levelConfig) error {
return setPunishmentForKey(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),
)
return errors.Wrap(err, "updating punishment info")
}
func getDBKey(channel, user, uuid string) string {
return strings.Join([]string{channel, user, uuid}, "::")
}

View file

@ -0,0 +1,6 @@
CREATE TABLE punish_levels (
key STRING NOT NULL PRIMARY KEY,
last_level INTEGER,
executed INTEGER, -- time.Time
cooldown INTEGER -- time.Duration
);

View file

@ -1,14 +1,12 @@
package quotedb
import (
"encoding/json"
"math/rand"
"strconv"
"sync"
"github.com/go-irc/irc"
"github.com/pkg/errors"
"github.com/Luzifer/twitch-bot/pkg/database"
"github.com/Luzifer/twitch-bot/plugins"
)
@ -18,9 +16,8 @@ const (
)
var (
db database.Connector
formatMessage plugins.MsgFormatter
store plugins.StorageManager
storedObject = newStorage()
ptrStringEmpty = func(v string) *string { return &v }("")
ptrStringOutFormat = func(v string) *string { return &v }("Quote #{{ .index }}: {{ .quote }}")
@ -28,8 +25,12 @@ var (
)
func Register(args plugins.RegistrationArguments) error {
db = args.GetDatabaseConnector()
if err := db.Migrate(actorName, database.NewEmbedFSMigrator(schema, "schema")); err != nil {
return errors.Wrap(err, "applying schema migration")
}
formatMessage = args.FormatMessage
store = args.GetStorageManager()
args.RegisterActor(actorName, func() plugins.Actor { return &actor{} })
@ -81,25 +82,16 @@ func Register(args plugins.RegistrationArguments) error {
registerAPI(args.RegisterAPIRoute)
args.RegisterTemplateFunction("lastQuoteIndex", func(m *irc.Message, r *plugins.Rule, fields *plugins.FieldCollection) interface{} {
return func() int {
return storedObject.GetMaxQuoteIdx(plugins.DeriveChannel(m, nil))
return func() (int, error) {
return getMaxQuoteIdx(plugins.DeriveChannel(m, nil))
}
})
return errors.Wrap(
store.GetModuleStore(moduleUUID, storedObject),
"loading module storage",
)
return nil
}
type (
actor struct{}
storage struct {
ChannelQuotes map[string][]string `json:"channel_quotes"`
lock sync.RWMutex
}
)
func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
@ -129,21 +121,22 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
return false, errors.Wrap(err, "formatting quote")
}
storedObject.AddQuote(plugins.DeriveChannel(m, eventData), quote)
return false, errors.Wrap(
store.SetModuleStore(moduleUUID, storedObject),
"storing quote database",
addQuote(plugins.DeriveChannel(m, eventData), quote),
"adding quote",
)
case "del":
storedObject.DelQuote(plugins.DeriveChannel(m, eventData), index)
return false, errors.Wrap(
store.SetModuleStore(moduleUUID, storedObject),
delQuote(plugins.DeriveChannel(m, eventData), index),
"storing quote database",
)
case "get":
idx, quote := storedObject.GetQuote(plugins.DeriveChannel(m, eventData), index)
idx, quote, err := getQuote(plugins.DeriveChannel(m, eventData), index)
if err != nil {
return false, errors.Wrap(err, "getting quote")
}
if idx == 0 {
// No quote was found for the given idx
@ -201,108 +194,3 @@ func (a actor) Validate(attrs *plugins.FieldCollection) (err error) {
return nil
}
// Storage
func newStorage() *storage {
return &storage{
ChannelQuotes: make(map[string][]string),
}
}
func (s *storage) AddQuote(channel, quote string) {
s.lock.Lock()
defer s.lock.Unlock()
s.ChannelQuotes[channel] = append(s.ChannelQuotes[channel], quote)
}
func (s *storage) DelQuote(channel string, quote int) {
s.lock.Lock()
defer s.lock.Unlock()
var quotes []string
for i, q := range s.ChannelQuotes[channel] {
if i == quote {
continue
}
quotes = append(quotes, q)
}
s.ChannelQuotes[channel] = quotes
}
func (s *storage) GetChannelQuotes(channel string) []string {
s.lock.RLock()
defer s.lock.RUnlock()
var out []string
out = append(out, s.ChannelQuotes[channel]...)
return out
}
func (s *storage) GetMaxQuoteIdx(channel string) int {
s.lock.RLock()
defer s.lock.RUnlock()
return len(s.ChannelQuotes[channel])
}
func (s *storage) GetQuote(channel string, quote int) (int, string) {
s.lock.RLock()
defer s.lock.RUnlock()
if quote == 0 {
quote = rand.Intn(len(s.ChannelQuotes[channel])) + 1 // #nosec G404 // no need for cryptographic safety
}
if quote > len(s.ChannelQuotes[channel]) {
return 0, ""
}
return quote, s.ChannelQuotes[channel][quote-1]
}
func (s *storage) SetQuotes(channel string, quotes []string) {
s.lock.Lock()
defer s.lock.Unlock()
s.ChannelQuotes[channel] = quotes
}
func (s *storage) UpdateQuote(channel string, idx int, quote string) {
s.lock.Lock()
defer s.lock.Unlock()
var quotes []string
for i := range s.ChannelQuotes[channel] {
if i == idx {
quotes = append(quotes, quote)
continue
}
quotes = append(quotes, s.ChannelQuotes[channel][i])
}
s.ChannelQuotes[channel] = quotes
}
// Implement marshaller interfaces
func (s *storage) MarshalStoredObject() ([]byte, error) {
s.lock.RLock()
defer s.lock.RUnlock()
return json.Marshal(s)
}
func (s *storage) UnmarshalStoredObject(data []byte) error {
if data == nil {
// No data set yet, don't try to unmarshal
return nil
}
s.lock.Lock()
defer s.lock.Unlock()
return json.Unmarshal(data, s)
}

View file

@ -0,0 +1,173 @@
package quotedb
import (
"database/sql"
"embed"
"math/rand"
"time"
"github.com/pkg/errors"
)
//go:embed schema/**
var schema embed.FS
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,
)
return errors.Wrap(err, "adding quote to database")
}
func delQuote(channel string, quote int) error {
_, createdAt, _, err := getQuoteRaw(channel, quote)
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(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 {
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(&quote); err != nil {
return nil, errors.Wrap(err, "scanning row")
}
quotes = append(quotes, quote)
}
return quotes, errors.Wrap(rows.Err(), "advancing row read")
}
func getMaxQuoteIdx(channel string) (int, error) {
row := db.DB().QueryRow(
`SELECT COUNT(1) as quoteCount
FROM quotedb
WHERE channel = $1;`,
channel,
)
var count int
err := row.Scan(&count)
return count, errors.Wrap(err, "getting quote count")
}
func getQuote(channel string, quote int) (int, string, error) {
quoteIdx, _, quoteText, err := getQuoteRaw(channel, quote)
return quoteIdx, quoteText, err
}
func getQuoteRaw(channel string, quote int) (int, int64, string, error) {
if quote == 0 {
max, err := getMaxQuoteIdx(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
}
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 (
createdAt int64
quoteText string
)
err := row.Scan(&createdAt, &quoteText)
switch {
case err == nil:
return quote, createdAt, quoteText, nil
case errors.Is(err, sql.ErrNoRows):</