mirror of
https://github.com/Luzifer/twitch-bot.git
synced 2025-02-07 02:52:20 +00:00
parent
c1a7221b06
commit
a7533cbd8b
72 changed files with 2127 additions and 921 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -6,5 +6,7 @@ editor/app.js
|
|||
editor/bundle.*
|
||||
.env
|
||||
node_modules
|
||||
storage.db
|
||||
storage.db-journal
|
||||
storage.json.gz
|
||||
twitch-bot
|
||||
|
|
42
README.md
42
README.md
|
@ -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>
|
||||
```
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
16
auth.go
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
|
||||
"github.com/go-irc/irc"
|
||||
|
||||
"github.com/Luzifer/twitch-bot/twitch"
|
||||
"github.com/Luzifer/twitch-bot/pkg/twitch"
|
||||
)
|
||||
|
||||
type (
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}))
|
||||
}
|
||||
|
|
|
@ -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
11
go.mod
|
@ -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
49
go.sum
|
@ -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=
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
55
internal/actors/counter/database.go
Normal file
55
internal/actors/counter/database.go
Normal 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")
|
||||
}
|
4
internal/actors/counter/schema/001.sql
Normal file
4
internal/actors/counter/schema/001.sql
Normal file
|
@ -0,0 +1,4 @@
|
|||
CREATE TABLE counters (
|
||||
name STRING NOT NULL PRIMARY KEY,
|
||||
value INTEGER
|
||||
);
|
|
@ -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"
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
142
internal/actors/punish/database.go
Normal file
142
internal/actors/punish/database.go
Normal 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}, "::")
|
||||
}
|
6
internal/actors/punish/schema/001.sql
Normal file
6
internal/actors/punish/schema/001.sql
Normal 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
|
||||
);
|
|
@ -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)
|
||||
}
|
||||
|
|
173
internal/actors/quotedb/database.go
Normal file
173
internal/actors/quotedb/database.go
Normal 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("e); 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, "eText)
|
||||
switch {
|
||||
case err == nil:
|
||||
return quote, createdAt, quoteText, nil
|
||||
|
||||
case errors.Is(err, sql.ErrNoRows): |