mirror of
https://github.com/Luzifer/twitch-bot.git
synced 2024-12-30 08:31:16 +00:00
Compare commits
12 commits
9e2510ec09
...
4a05a8a7f2
Author | SHA1 | Date | |
---|---|---|---|
4a05a8a7f2 | |||
6f3a2b6193 | |||
0ebc68254d | |||
1298b76da9 | |||
29b0e41218 | |||
c78356f68f | |||
7189232093 | |||
4fdcd86dee | |||
e1434eb403 | |||
fa9f5591f6 | |||
eb02858280 | |||
61bab3c984 |
139 changed files with 1782 additions and 1205 deletions
140
.golangci.yml
140
.golangci.yml
|
@ -12,34 +12,27 @@ output:
|
|||
format: tab
|
||||
|
||||
issues:
|
||||
# This disables the included exclude-list in golangci-lint as that
|
||||
# list for example fully hides G304 gosec rule, errcheck, exported
|
||||
# rule of revive and other errors one really wants to see.
|
||||
# Smme detail: https://github.com/golangci/golangci-lint/issues/456
|
||||
exclude-use-default: false
|
||||
# Don't limit the number of shown issues: Report ALL of them
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
|
||||
linters-settings:
|
||||
forbidigo:
|
||||
forbid:
|
||||
- 'fmt\.Errorf' # Should use github.com/pkg/errors
|
||||
|
||||
funlen:
|
||||
lines: 100
|
||||
statements: 60
|
||||
|
||||
gocyclo:
|
||||
# minimal code complexity to report, 30 by default (but we recommend 10-20)
|
||||
min-complexity: 15
|
||||
|
||||
gomnd:
|
||||
settings:
|
||||
mnd:
|
||||
ignored-functions: 'strconv.(?:Format|Parse)\B+'
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers [fast: true, auto-fix: false]
|
||||
- bidichk # Checks for dangerous unicode character sequences [fast: true, auto-fix: false]
|
||||
- bodyclose # checks whether HTTP response body is closed successfully [fast: true, auto-fix: false]
|
||||
- containedctx # containedctx is a linter that detects struct contained context.Context field [fast: true, auto-fix: false]
|
||||
- contextcheck # check the function whether use a non-inherited context [fast: false, auto-fix: false]
|
||||
- dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) [fast: true, auto-fix: false]
|
||||
- durationcheck # check for two durations multiplied together [fast: false, auto-fix: false]
|
||||
- errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases [fast: false, auto-fix: false]
|
||||
- errchkjson # Checks types passed to the json encoding functions. Reports unsupported types and optionally reports occations, where the check for the returned error can be omitted. [fast: false, auto-fix: false]
|
||||
- exportloopref # checks for pointers to enclosing loop variables [fast: true, auto-fix: false]
|
||||
- forbidigo # Forbids identifiers [fast: true, auto-fix: false]
|
||||
- funlen # Tool for detection of long functions [fast: true, auto-fix: false]
|
||||
|
@ -58,13 +51,124 @@ linters:
|
|||
- ineffassign # Detects when assignments to existing variables are not used [fast: true, auto-fix: false]
|
||||
- misspell # Finds commonly misspelled English words in comments [fast: true, auto-fix: true]
|
||||
- nakedret # Finds naked returns in functions greater than a specified function length [fast: true, auto-fix: false]
|
||||
- nilerr # Finds the code that returns nil even if it checks that the error is not nil. [fast: false, auto-fix: false]
|
||||
- nilnil # Checks that there is no simultaneous return of `nil` error and an invalid value. [fast: false, auto-fix: false]
|
||||
- noctx # noctx finds sending http request without context.Context [fast: true, auto-fix: false]
|
||||
- nolintlint # Reports ill-formed or insufficient nolint directives [fast: true, auto-fix: false]
|
||||
- revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint. [fast: false, auto-fix: false]
|
||||
- staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks [fast: true, auto-fix: false]
|
||||
- stylecheck # Stylecheck is a replacement for golint [fast: true, auto-fix: false]
|
||||
- tenv # tenv is analyzer that detects using os.Setenv instead of t.Setenv since Go1.17 [fast: false, auto-fix: false]
|
||||
- typecheck # Like the front-end of a Go compiler, parses and type-checks Go code [fast: true, auto-fix: false]
|
||||
- unconvert # Remove unnecessary type conversions [fast: true, auto-fix: false]
|
||||
- unused # Checks Go code for unused constants, variables, functions and types [fast: false, auto-fix: false]
|
||||
- wastedassign # wastedassign finds wasted assignment statements. [fast: false, auto-fix: false]
|
||||
- wrapcheck # Checks that errors returned from external packages are wrapped [fast: false, auto-fix: false]
|
||||
|
||||
linters-settings:
|
||||
funlen:
|
||||
lines: 100
|
||||
statements: 60
|
||||
|
||||
gocyclo:
|
||||
# minimal code complexity to report, 30 by default (but we recommend 10-20)
|
||||
min-complexity: 15
|
||||
|
||||
gomnd:
|
||||
settings:
|
||||
mnd:
|
||||
ignored-functions: 'strconv.(?:Format|Parse)\B+'
|
||||
|
||||
revive:
|
||||
rules:
|
||||
#- name: add-constant # Suggests using constant for magic numbers and string literals
|
||||
# Opinion: Makes sense for strings, not for numbers but checks numbers
|
||||
#- name: argument-limit # Specifies the maximum number of arguments a function can receive | Opinion: Don't need this
|
||||
- name: atomic # Check for common mistaken usages of the `sync/atomic` package
|
||||
- name: banned-characters # Checks banned characters in identifiers
|
||||
arguments:
|
||||
- ';' # Greek question mark
|
||||
- name: bare-return # Warns on bare returns
|
||||
- name: blank-imports # Disallows blank imports
|
||||
- name: bool-literal-in-expr # Suggests removing Boolean literals from logic expressions
|
||||
- name: call-to-gc # Warns on explicit call to the garbage collector
|
||||
#- name: cognitive-complexity # Sets restriction for maximum Cognitive complexity.
|
||||
# There is a dedicated linter for this
|
||||
- name: confusing-naming # Warns on methods with names that differ only by capitalization
|
||||
- name: confusing-results # Suggests to name potentially confusing function results
|
||||
- name: constant-logical-expr # Warns on constant logical expressions
|
||||
- name: context-as-argument # `context.Context` should be the first argument of a function.
|
||||
- name: context-keys-type # Disallows the usage of basic types in `context.WithValue`.
|
||||
#- name: cyclomatic # Sets restriction for maximum Cyclomatic complexity.
|
||||
# There is a dedicated linter for this
|
||||
#- name: datarace # Spots potential dataraces
|
||||
# Is not (yet) available?
|
||||
- name: deep-exit # Looks for program exits in funcs other than `main()` or `init()`
|
||||
- name: defer # Warns on some [defer gotchas](https://blog.learngoprogramming.com/5-gotchas-of-defer-in-go-golang-part-iii-36a1ab3d6ef1)
|
||||
- name: dot-imports # Forbids `.` imports.
|
||||
- name: duplicated-imports # Looks for packages that are imported two or more times
|
||||
- name: early-return # Spots if-then-else statements that can be refactored to simplify code reading
|
||||
- name: empty-block # Warns on empty code blocks
|
||||
- name: empty-lines # Warns when there are heading or trailing newlines in a block
|
||||
- name: errorf # Should replace `errors.New(fmt.Sprintf())` with `fmt.Errorf()`
|
||||
- name: error-naming # Naming of error variables.
|
||||
- name: error-return # The error return parameter should be last.
|
||||
- name: error-strings # Conventions around error strings.
|
||||
- name: exported # Naming and commenting conventions on exported symbols.
|
||||
arguments: ['sayRepetitiveInsteadOfStutters']
|
||||
#- name: file-header # Header which each file should have.
|
||||
# Useless without config, have no config for it
|
||||
- name: flag-parameter # Warns on boolean parameters that create a control coupling
|
||||
#- name: function-length # Warns on functions exceeding the statements or lines max
|
||||
# There is a dedicated linter for this
|
||||
#- name: function-result-limit # Specifies the maximum number of results a function can return
|
||||
# Opinion: Don't need this
|
||||
- name: get-return # Warns on getters that do not yield any result
|
||||
- name: identical-branches # Spots if-then-else statements with identical `then` and `else` branches
|
||||
- name: if-return # Redundant if when returning an error.
|
||||
#- name: imports-blacklist # Disallows importing the specified packages
|
||||
# Useless without config, have no config for it
|
||||
- name: import-shadowing # Spots identifiers that shadow an import
|
||||
- name: increment-decrement # Use `i++` and `i--` instead of `i += 1` and `i -= 1`.
|
||||
- name: indent-error-flow # Prevents redundant else statements.
|
||||
#- name: line-length-limit # Specifies the maximum number of characters in a lined
|
||||
# There is a dedicated linter for this
|
||||
#- name: max-public-structs # The maximum number of public structs in a file.
|
||||
# Opinion: Don't need this
|
||||
- name: modifies-parameter # Warns on assignments to function parameters
|
||||
- name: modifies-value-receiver # Warns on assignments to value-passed method receivers
|
||||
#- name: nested-structs # Warns on structs within structs
|
||||
# Opinion: Don't need this
|
||||
- name: optimize-operands-order # Checks inefficient conditional expressions
|
||||
#- name: package-comments # Package commenting conventions.
|
||||
# Opinion: Don't need this
|
||||
- name: range # Prevents redundant variables when iterating over a collection.
|
||||
- name: range-val-address # Warns if address of range value is used dangerously
|
||||
- name: range-val-in-closure # Warns if range value is used in a closure dispatched as goroutine
|
||||
- name: receiver-naming # Conventions around the naming of receivers.
|
||||
- name: redefines-builtin-id # Warns on redefinitions of builtin identifiers
|
||||
#- name: string-format # Warns on specific string literals that fail one or more user-configured regular expressions
|
||||
# Useless without config, have no config for it
|
||||
- name: string-of-int # Warns on suspicious casts from int to string
|
||||
- name: struct-tag # Checks common struct tags like `json`,`xml`,`yaml`
|
||||
- name: superfluous-else # Prevents redundant else statements (extends indent-error-flow)
|
||||
- name: time-equal # Suggests to use `time.Time.Equal` instead of `==` and `!=` for equality check time.
|
||||
- name: time-naming # Conventions around the naming of time variables.
|
||||
- name: unconditional-recursion # Warns on function calls that will lead to (direct) infinite recursion
|
||||
- name: unexported-naming # Warns on wrongly named un-exported symbols
|
||||
- name: unexported-return # Warns when a public return is from unexported type.
|
||||
- name: unhandled-error # Warns on unhandled errors returned by funcion calls
|
||||
arguments:
|
||||
- "fmt.(Fp|P)rint(f|ln|)"
|
||||
- name: unnecessary-stmt # Suggests removing or simplifying unnecessary statements
|
||||
- name: unreachable-code # Warns on unreachable code
|
||||
- name: unused-parameter # Suggests to rename or remove unused function parameters
|
||||
- name: unused-receiver # Suggests to rename or remove unused method receivers
|
||||
#- name: use-any # Proposes to replace `interface{}` with its alias `any`
|
||||
# Is not (yet) available?
|
||||
- name: useless-break # Warns on useless `break` statements in case clauses
|
||||
- name: var-declaration # Reduces redundancies around variable declaration.
|
||||
- name: var-naming # Naming rules.
|
||||
- name: waitgroup-by-value # Warns on functions taking sync.WaitGroup as a by-value parameter
|
||||
|
||||
...
|
||||
|
|
33
History.md
33
History.md
|
@ -1,3 +1,36 @@
|
|||
# 3.24.1 / 2024-01-24
|
||||
|
||||
* New Features
|
||||
* [core] Add support for `watch_streak` event
|
||||
* [overlays] Add support for replaying events
|
||||
|
||||
* Improvements
|
||||
* [linkcheck] Refactor: Improve wait-code
|
||||
* [overlays] Add WebDAV support for remote Overlay editing
|
||||
|
||||
* Bugfixes
|
||||
* [ci] Lint: Update linter config, improve code quality
|
||||
* [core] Update dependencies
|
||||
* [eventsub] Fix: Log error when giving up subscription retries
|
||||
* [linkcheck] Fix tests broken by domain grabbers
|
||||
* [overlays] Fix: Do not spam logs with errors when overlay reloaded
|
||||
|
||||
# 3.24.0 / 2024-01-24
|
||||
|
||||
* New Features
|
||||
* [core] Add support for `watch_streak` event
|
||||
* [overlays] Add support for replaying events
|
||||
|
||||
* Improvements
|
||||
* [linkcheck] Refactor: Improve wait-code
|
||||
* [overlays] Add WebDAV support for remote Overlay editing
|
||||
|
||||
* Bugfixes
|
||||
* [ci] Lint: Update linter config, improve code quality
|
||||
* [core] Update dependencies
|
||||
* [eventsub] Fix: Log error when giving up subscription retries
|
||||
* [overlays] Fix: Do not spam logs with errors when overlay reloaded
|
||||
|
||||
# 3.23.1 / 2023-12-20
|
||||
|
||||
* Bugfixes
|
||||
|
|
2
Makefile
2
Makefile
|
@ -56,7 +56,7 @@ trivy:
|
|||
|
||||
# -- Documentation Site --
|
||||
|
||||
docs: actor_docs template_docs
|
||||
docs: actor_docs eventclient_docs template_docs
|
||||
|
||||
actor_docs:
|
||||
go run . --storage-conn-string $(shell mktemp).db actor-docs >docs/content/configuration/actors.md
|
||||
|
|
|
@ -45,8 +45,10 @@ func init() {
|
|||
})
|
||||
}
|
||||
|
||||
// ActorScript contains an actor to execute arbitrary commands and scripts
|
||||
type ActorScript struct{}
|
||||
|
||||
// Execute implements actor interface
|
||||
func (ActorScript) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
command, err := attrs.StringSlice("command")
|
||||
if err != nil {
|
||||
|
@ -121,9 +123,13 @@ func (ActorScript) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, event
|
|||
return preventCooldown, nil
|
||||
}
|
||||
|
||||
// IsAsync implements actor interface
|
||||
func (ActorScript) IsAsync() bool { return false }
|
||||
func (ActorScript) Name() string { return "script" }
|
||||
|
||||
// Name implements actor interface
|
||||
func (ActorScript) Name() string { return "script" }
|
||||
|
||||
// Validate implements actor interface
|
||||
func (ActorScript) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
cmd, err := attrs.StringSlice("command")
|
||||
if err != nil || len(cmd) == 0 {
|
||||
|
|
20
auth.go
20
auth.go
|
@ -9,7 +9,7 @@ import (
|
|||
|
||||
"github.com/gofrs/uuid/v3"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||
|
@ -39,7 +39,7 @@ func init() {
|
|||
},
|
||||
} {
|
||||
if err := registerRoute(rd); err != nil {
|
||||
log.WithError(err).Fatal("Unable to register auth routes")
|
||||
logrus.WithError(err).Fatal("Unable to register auth routes")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -71,7 +71,11 @@ func handleAuthUpdateBotToken(w http.ResponseWriter, r *http.Request) {
|
|||
http.Error(w, errors.Wrap(err, "getting access token").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
logrus.WithError(err).Error("closing response body (leaked fd)")
|
||||
}
|
||||
}()
|
||||
|
||||
var rData twitch.OAuthTokenResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&rData); err != nil {
|
||||
|
@ -79,7 +83,7 @@ func handleAuthUpdateBotToken(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
_, botUser, err := twitch.New(cfg.TwitchClient, cfg.TwitchClientSecret, rData.AccessToken, "").GetAuthorizedUser()
|
||||
_, botUser, err := twitch.New(cfg.TwitchClient, cfg.TwitchClientSecret, rData.AccessToken, "").GetAuthorizedUser(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
@ -129,7 +133,11 @@ func handleAuthUpdateChannelGrant(w http.ResponseWriter, r *http.Request) {
|
|||
http.Error(w, errors.Wrap(err, "getting access token").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
logrus.WithError(err).Error("closing response body (leaked fd)")
|
||||
}
|
||||
}()
|
||||
|
||||
var rData twitch.OAuthTokenResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&rData); err != nil {
|
||||
|
@ -137,7 +145,7 @@ func handleAuthUpdateChannelGrant(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
_, grantUser, err := twitch.New(cfg.TwitchClient, cfg.TwitchClientSecret, rData.AccessToken, "").GetAuthorizedUser()
|
||||
_, grantUser, err := twitch.New(cfg.TwitchClient, cfg.TwitchClientSecret, rData.AccessToken, "").GetAuthorizedUser(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
|
|
@ -31,7 +31,7 @@ func authBackendTwitchToken(token string) (modules []string, expiresAt time.Time
|
|||
|
||||
var httpError twitch.HTTPError
|
||||
|
||||
id, user, err := tc.GetAuthorizedUser()
|
||||
id, user, err := tc.GetAuthorizedUser(context.Background())
|
||||
switch {
|
||||
case err == nil:
|
||||
// We got a valid user, continue check below
|
||||
|
|
|
@ -43,8 +43,17 @@ func fillAuthToken(token *configAuthToken) error {
|
|||
|
||||
func writeAuthMiddleware(h http.Handler, module string) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.Header.Get("Authorization")
|
||||
if token == "" {
|
||||
_, pass, hasBasicAuth := r.BasicAuth()
|
||||
|
||||
var token string
|
||||
switch {
|
||||
case hasBasicAuth && pass != "":
|
||||
token = pass
|
||||
|
||||
case r.Header.Get("Authorization") != "":
|
||||
token = r.Header.Get("Authorization")
|
||||
|
||||
default:
|
||||
http.Error(w, "auth not successful", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
@ -71,7 +72,7 @@ func (a *autoMessage) CanSend() bool {
|
|||
}
|
||||
|
||||
if a.OnlyOnLive {
|
||||
streamLive, err := twitchClient.HasLiveStream(strings.TrimLeft(a.Channel, "#"))
|
||||
streamLive, err := twitchClient.HasLiveStream(context.Background(), strings.TrimLeft(a.Channel, "#"))
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Unable to determine channel live status")
|
||||
return false
|
||||
|
|
|
@ -16,6 +16,6 @@ func getAuthorizationFromRequest(r *http.Request) (string, *twitch.Client, error
|
|||
|
||||
tc := twitch.New(cfg.TwitchClient, cfg.TwitchClientSecret, token, "")
|
||||
|
||||
_, user, err := tc.GetAuthorizedUser()
|
||||
_, user, err := tc.GetAuthorizedUser(r.Context())
|
||||
return user, tc, errors.Wrap(err, "getting authorized user")
|
||||
}
|
||||
|
|
60
config.go
60
config.go
|
@ -13,7 +13,7 @@ import (
|
|||
|
||||
"github.com/gofrs/uuid/v3"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/argon2"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gopkg.in/irc.v4"
|
||||
|
@ -23,7 +23,11 @@ import (
|
|||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||
)
|
||||
|
||||
const expectedMinConfigVersion = 2
|
||||
const (
|
||||
expectedMinConfigVersion = 2
|
||||
rawLogDirPerm = 0o755
|
||||
rawLogFilePerm = 0o644
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed default_config.yaml
|
||||
|
@ -121,10 +125,10 @@ func loadConfig(filename string) error {
|
|||
if err = config.CloseRawMessageWriter(); err != nil {
|
||||
return errors.Wrap(err, "closing old raw log writer")
|
||||
}
|
||||
if err = os.MkdirAll(path.Dir(tmpConfig.RawLog), 0o755); err != nil { //nolint:gomnd // This is a common directory permission
|
||||
if err = os.MkdirAll(path.Dir(tmpConfig.RawLog), rawLogDirPerm); err != nil {
|
||||
return errors.Wrap(err, "creating directories for raw log")
|
||||
}
|
||||
if tmpConfig.rawLogWriter, err = os.OpenFile(tmpConfig.RawLog, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644); err != nil { //nolint:gomnd // This is a common file permission
|
||||
if tmpConfig.rawLogWriter, err = os.OpenFile(tmpConfig.RawLog, os.O_APPEND|os.O_CREATE|os.O_WRONLY, rawLogFilePerm); err != nil {
|
||||
return errors.Wrap(err, "opening raw log for appending")
|
||||
}
|
||||
}
|
||||
|
@ -132,7 +136,7 @@ func loadConfig(filename string) error {
|
|||
config = tmpConfig
|
||||
timerService.UpdatePermitTimeout(tmpConfig.PermitTimeout)
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"auto_messages": len(config.AutoMessages),
|
||||
"rules": len(config.Rules),
|
||||
"channels": len(config.Channels),
|
||||
|
@ -145,11 +149,15 @@ func loadConfig(filename string) error {
|
|||
}
|
||||
|
||||
func parseConfigFromYAML(filename string, obj interface{}, strict bool) error {
|
||||
f, err := os.Open(filename)
|
||||
f, err := os.Open(filename) //#nosec:G304 // This is intended to open a variable file
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "open config file")
|
||||
}
|
||||
defer f.Close()
|
||||
defer func() {
|
||||
if err := f.Close(); err != nil {
|
||||
logrus.WithError(err).Error("closing config file (leaked fd)")
|
||||
}
|
||||
}()
|
||||
|
||||
decoder := yaml.NewDecoder(f)
|
||||
decoder.KnownFields(strict)
|
||||
|
@ -205,10 +213,13 @@ func writeConfigToYAML(filename, authorName, authorEmail, summary string, obj *c
|
|||
fmt.Fprintf(tmpFile, "# Automatically updated by %s using Config-Editor frontend, last update: %s\n", authorName, time.Now().Format(time.RFC3339))
|
||||
|
||||
if err = yaml.NewEncoder(tmpFile).Encode(obj); err != nil {
|
||||
tmpFile.Close()
|
||||
tmpFile.Close() //nolint:errcheck,gosec,revive
|
||||
return errors.Wrap(err, "encoding config")
|
||||
}
|
||||
tmpFile.Close()
|
||||
|
||||
if err = tmpFile.Close(); err != nil {
|
||||
return fmt.Errorf("closing temp config: %w", err)
|
||||
}
|
||||
|
||||
if err = os.Rename(tmpFileName, filename); err != nil {
|
||||
return errors.Wrap(err, "moving config to location")
|
||||
|
@ -220,7 +231,7 @@ func writeConfigToYAML(filename, authorName, authorEmail, summary string, obj *c
|
|||
|
||||
git := newGitHelper(path.Dir(filename))
|
||||
if !git.HasRepo() {
|
||||
log.Error("Instructed to track changes using Git, but config not in repo")
|
||||
logrus.Error("Instructed to track changes using Git, but config not in repo")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -231,11 +242,15 @@ func writeConfigToYAML(filename, authorName, authorEmail, summary string, obj *c
|
|||
}
|
||||
|
||||
func writeDefaultConfigFile(filename string) error {
|
||||
f, err := os.Create(filename)
|
||||
f, err := os.Create(filename) //#nosec:G304 // This is intended to open a variable file
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "creating config file")
|
||||
}
|
||||
defer f.Close()
|
||||
defer func() {
|
||||
if err := f.Close(); err != nil {
|
||||
logrus.WithError(err).Error("closing config file (leaked fd)")
|
||||
}
|
||||
}()
|
||||
|
||||
_, err = f.Write(defaultConfigurationYAML)
|
||||
return errors.Wrap(err, "writing default config")
|
||||
|
@ -276,11 +291,16 @@ func (c configAuthToken) validate(token string) error {
|
|||
}
|
||||
}
|
||||
|
||||
func (c *configFile) CloseRawMessageWriter() error {
|
||||
func (c *configFile) CloseRawMessageWriter() (err error) {
|
||||
if c == nil || c.rawLogWriter == nil {
|
||||
return nil
|
||||
}
|
||||
return c.rawLogWriter.Close()
|
||||
|
||||
if err = c.rawLogWriter.Close(); err != nil {
|
||||
return fmt.Errorf("closing raw-log writer: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c configFile) GetMatchingRules(m *irc.Message, event *string, eventData *plugins.FieldCollection) []*plugins.Rule {
|
||||
|
@ -319,14 +339,14 @@ func (configFile) fixedDuration(d time.Duration) time.Duration {
|
|||
if d > time.Second {
|
||||
return d
|
||||
}
|
||||
return d * time.Second
|
||||
return d * time.Second //nolint:durationcheck // Error is handled before
|
||||
}
|
||||
|
||||
func (configFile) fixedDurationPtr(d *time.Duration) *time.Duration {
|
||||
if d == nil || *d >= time.Second {
|
||||
return d
|
||||
}
|
||||
fd := *d * time.Second
|
||||
fd := *d * time.Second //nolint:durationcheck // Error is handled before
|
||||
return &fd
|
||||
}
|
||||
|
||||
|
@ -368,11 +388,11 @@ func (c *configFile) fixTokenHashStorage() (err error) {
|
|||
|
||||
func (c *configFile) runLoadChecks() (err error) {
|
||||
if len(c.Channels) == 0 {
|
||||
log.Warn("Loaded config with empty channel list")
|
||||
logrus.Warn("Loaded config with empty channel list")
|
||||
}
|
||||
|
||||
if len(c.Rules) == 0 {
|
||||
log.Warn("Loaded config with empty ruleset")
|
||||
logrus.Warn("Loaded config with empty ruleset")
|
||||
}
|
||||
|
||||
var seen []string
|
||||
|
@ -397,7 +417,7 @@ func (c *configFile) updateAutoMessagesFromConfig(old *configFile) {
|
|||
nam.lastMessageSent = time.Now()
|
||||
|
||||
if !nam.IsValid() {
|
||||
log.WithField("index", idx).Warn("Auto-Message configuration is invalid and therefore disabled")
|
||||
logrus.WithField("index", idx).Warn("Auto-Message configuration is invalid and therefore disabled")
|
||||
}
|
||||
|
||||
if old == nil {
|
||||
|
@ -426,7 +446,7 @@ func (c configFile) validateRuleActions() error {
|
|||
var hasError bool
|
||||
|
||||
for _, r := range c.Rules {
|
||||
logger := log.WithField("rule", r.MatcherID())
|
||||
logger := logrus.WithField("rule", r.MatcherID())
|
||||
|
||||
if err := r.Validate(validateTemplate); err != nil {
|
||||
logger.WithError(err).Error("Rule reported invalid config")
|
||||
|
|
|
@ -53,7 +53,7 @@ func registerEditorFrontend() {
|
|||
return
|
||||
}
|
||||
|
||||
io.Copy(w, f)
|
||||
io.Copy(w, f) //nolint:errcheck,gosec
|
||||
})
|
||||
|
||||
router.HandleFunc("/editor/vars.json", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
@ -244,7 +244,7 @@ func configEditorHandleGeneralUpdate(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
for i := range payload.BotEditors {
|
||||
usr, err := twitchClient.GetUserInformation(payload.BotEditors[i])
|
||||
usr, err := twitchClient.GetUserInformation(r.Context(), payload.BotEditors[i])
|
||||
if err != nil {
|
||||
http.Error(w, errors.Wrap(err, "getting bot editor profile").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
|
|
@ -143,7 +143,7 @@ func configEditorGlobalGetModules(w http.ResponseWriter, _ *http.Request) {
|
|||
}
|
||||
|
||||
func configEditorGlobalGetUser(w http.ResponseWriter, r *http.Request) {
|
||||
usr, err := twitchClient.GetUserInformation(r.FormValue("user"))
|
||||
usr, err := twitchClient.GetUserInformation(r.Context(), r.FormValue("user"))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
@ -160,7 +160,7 @@ func configEditorGlobalSubscribe(w http.ResponseWriter, r *http.Request) {
|
|||
log.WithError(err).Error("Unable to initialize websocket")
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
defer conn.Close() //nolint:errcheck
|
||||
|
||||
var (
|
||||
frontendNotify = make(chan string, 1)
|
||||
|
@ -190,7 +190,6 @@ func configEditorGlobalSubscribe(w http.ResponseWriter, r *http.Request) {
|
|||
log.WithError(err).Debug("Unable to send websocket ping")
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -90,7 +90,7 @@ func configEditorRulesAdd(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
if msg.SubscribeFrom != nil {
|
||||
if _, err = msg.UpdateFromSubscription(); err != nil {
|
||||
if _, err = msg.UpdateFromSubscription(r.Context()); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
|
||||
|
@ -24,7 +25,7 @@ func updateConfigFromRemote() {
|
|||
for _, r := range cfg.Rules {
|
||||
logger := log.WithField("rule", r.MatcherID())
|
||||
|
||||
rhu, err := r.UpdateFromSubscription()
|
||||
rhu, err := r.UpdateFromSubscription(context.Background())
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("updating rule")
|
||||
continue
|
||||
|
|
|
@ -502,7 +502,7 @@ Scans for links in the message and adds the "links" field to the event data
|
|||
```yaml
|
||||
- type: linkdetector
|
||||
attributes:
|
||||
# Enable heuristic scans to find links with spaces or other means of obfuscation in them
|
||||
# Enable heuristic scans to find links with spaces or other means of obfuscation in them (quite slow and will detect MANY false-positive links, only use for blacklisting links!)
|
||||
# Optional: true
|
||||
# Type: bool
|
||||
heuristic: false
|
||||
|
|
|
@ -441,7 +441,7 @@ Example:
|
|||
|
||||
```
|
||||
# Your int this hour: {{ printf "%.0f" (mulf (seededRandom (list "int" .username (now | date "2006-01-02 15") | join ":")) 100) }}%
|
||||
< Your int this hour: 73%
|
||||
< Your int this hour: 66%
|
||||
```
|
||||
|
||||
### `streamUptime`
|
||||
|
|
|
@ -40,3 +40,25 @@ Here you can see the Debug Overlay configured with:
|
|||
As those parameters are configured through the URL hash (`#...`) they are never sent to the server, therefore are not logged in any access-logs and exist only in the local URL. So with a custom overlay you would put `https://your-bot.example.com/overlays/myoverlay.html#token=55cdb1e4-c776-4467-8560-a47a4abc55de` into your OBS browser source and your overlay would be able to communicate with the bot.
|
||||
|
||||
The debug-overlay can be used to view all events received within the bot you can react on in overlays and bot rules.
|
||||
|
||||
## Remote editing Overlays with local Editor
|
||||
|
||||
In order to enable you to edit the overlays remotely when hosting the bot on a server the bot exposes a WebDAV interface you can locally mount and work on using your favorite editor. To mount the WebDAV I recommend [rclone](https://rclone.org/). You will need the URL your bot is available at and a token with `overlays` permission:
|
||||
|
||||
```
|
||||
# rclone obscure 55cdb1e4-c776-4467-8560-a47a4abc55de
|
||||
MqO0FLdbg3txom2IpUMsVVIqnHwYDefms4EKRqoV1MGhCFkBmWnhvVRdqTyCSFtmvP-AYg
|
||||
|
||||
# cat /tmp/rclone.conf
|
||||
[bot]
|
||||
type = webdav
|
||||
url = https://your-bot.example.com/overlays/dav/
|
||||
user = dav
|
||||
pass = MqO0FLdbg3txom2IpUMsVVIqnHwYDefms4EKRqoV1MGhCFkBmWnhvVRdqTyCSFtmvP-AYg
|
||||
|
||||
# rclone --config /tmp/rclone.conf mount bot:/ /tmp/bot-overlays
|
||||
|
||||
# code /tmp/bot-overlays
|
||||
```
|
||||
|
||||
What I've done here is to obscure the token (`rclone` wants the token to be in an obscured format), create a config containing the WebDAV remote, mount the WebDAV remote to a local directory and open it with VSCode to edit the overlays. When saving the files locally `rclone` will upload them to the bot and refreshing the overlay in your browser / OBS will give you the new version.
|
||||
|
|
|
@ -17,6 +17,9 @@ weight: 10000
|
|||
<dt><a href="#Options">Options</a> : <code>Object</code></dt>
|
||||
<dd><p>Options to pass to the EventClient constructor</p>
|
||||
</dd>
|
||||
<dt><a href="#SocketMessage">SocketMessage</a> : <code>Object</code></dt>
|
||||
<dd><p>SocketMessage received for every event and passed to the new <code>(eventObj) => { ... }</code> handlers</p>
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<a name="EventClient"></a>
|
||||
|
@ -31,6 +34,7 @@ EventClient abstracts the connection to the bot websocket for events
|
|||
* [.apiBase()](#EventClient+apiBase) ⇒ <code>string</code>
|
||||
* [.paramOptionFallback(key, [fallback])](#EventClient+paramOptionFallback) ⇒ <code>\*</code>
|
||||
* [.renderTemplate(template)](#EventClient+renderTemplate) ⇒ <code>Promise</code>
|
||||
* [.replayEvent(eventId)](#EventClient+replayEvent) ⇒ <code>Promise</code>
|
||||
|
||||
<a name="new_EventClient_new"></a>
|
||||
|
||||
|
@ -74,6 +78,18 @@ Renders a given template using the bots msgformat API (supports all templating y
|
|||
| --- | --- | --- |
|
||||
| template | <code>string</code> | The template to render |
|
||||
|
||||
<a name="EventClient+replayEvent"></a>
|
||||
|
||||
### eventClient.replayEvent(eventId) ⇒ <code>Promise</code>
|
||||
Triggers a replay of the given event to all overlays currently listening for events. This event will have the `is_live` flag set to `false`.
|
||||
|
||||
**Kind**: instance method of [<code>EventClient</code>](#EventClient)
|
||||
**Returns**: <code>Promise</code> - Promise of the fetch request
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| eventId | <code>Number</code> | The ID of the event received through the SocketMessage object |
|
||||
|
||||
<a name="Options"></a>
|
||||
|
||||
## Options : <code>Object</code>
|
||||
|
@ -84,9 +100,26 @@ Options to pass to the EventClient constructor
|
|||
|
||||
| Name | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| [channel] | <code>string</code> | | Filter for specific channel events (format: `#channel`) |
|
||||
| [handlers] | <code>Object</code> | <code>{}</code> | Map event types to callback functions `(event, fields, time, live) => {...}` |
|
||||
| [maxReplayAge] | <code>number</code> | <code>-1</code> | Number of hours to replay the events for (-1 = infinite) |
|
||||
| [replay] | <code>boolean</code> | <code>false</code> | Request a replay at connect (requires channel to be set to a channel name) |
|
||||
| [token] | <code>string</code> | | API access token to use to connect to the WebSocket (if not set, must be provided through URL hash) |
|
||||
| [channel] | <code>String</code> | | Filter for specific channel events (format: `#channel`) |
|
||||
| [handlers] | <code>Object</code> | <code>{}</code> | Map event types to callback functions `(eventObj) => { ... }` (new) or `(event, fields, time, live) => {...}` (old) |
|
||||
| [maxReplayAge] | <code>Number</code> | <code>-1</code> | Number of hours to replay the events for (-1 = infinite) |
|
||||
| [replay] | <code>Boolean</code> | <code>false</code> | Request a replay at connect (requires channel to be set to a channel name) |
|
||||
| [token] | <code>String</code> | | API access token to use to connect to the WebSocket (if not set, must be provided through URL hash) |
|
||||
|
||||
<a name="SocketMessage"></a>
|
||||
|
||||
## SocketMessage : <code>Object</code>
|
||||
SocketMessage received for every event and passed to the new `(eventObj) => { ... }` handlers
|
||||
|
||||
**Kind**: global typedef
|
||||
**Properties**
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [event_id] | <code>Number</code> | UID of the event used to re-trigger an event |
|
||||
| [is_live] | <code>Boolean</code> | Whether the event was sent through a replay (false) or occurred live (true) |
|
||||
| [reason] | <code>String</code> | Reason of this message (one of `bulk-replay`, `live-event`, `single-replay`) |
|
||||
| [time] | <code>String</code> | RFC3339 timestamp of the event |
|
||||
| [type] | <code>String</code> | Event type (i.e. `raid`, `sub`, ...) |
|
||||
| [fields] | <code>Object</code> | string->any mapping of fields available for the event |
|
||||
|
||||
|
|
|
@ -42,6 +42,7 @@ var (
|
|||
eventTypeSubmysterygift = ptrStr("submysterygift")
|
||||
eventTypeSub = ptrStr("sub")
|
||||
eventTypeTimeout = ptrStr("timeout")
|
||||
eventTypeWatchStreak = ptrStr("watch_streak")
|
||||
eventTypeWhisper = ptrStr("whisper")
|
||||
|
||||
eventTypeTwitchCategoryUpdate = ptrStr("category_update")
|
||||
|
@ -76,6 +77,7 @@ var (
|
|||
eventTypeSubgift,
|
||||
eventTypeSubmysterygift,
|
||||
eventTypeTimeout,
|
||||
eventTypeWatchStreak,
|
||||
eventTypeWhisper,
|
||||
|
||||
eventTypeTwitchCategoryUpdate,
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/irc.v4"
|
||||
|
||||
"github.com/Luzifer/go_helpers/v2/str"
|
||||
|
@ -78,7 +78,7 @@ func (t *templateFuncProvider) Register(name string, fg plugins.TemplateFuncGett
|
|||
defer t.lock.Unlock()
|
||||
|
||||
if _, ok := t.funcs[name]; ok {
|
||||
log.Fatalf("Duplicate registration of %q template function", name) //nolint:gocritic // Yeah, the unlock will not run but the process will end
|
||||
logrus.Fatalf("Duplicate registration of %q template function", name)
|
||||
}
|
||||
|
||||
t.funcs[name] = fg
|
||||
|
@ -108,7 +108,7 @@ func init() {
|
|||
var parts []string
|
||||
for idx, div := range []time.Duration{time.Hour, time.Minute, time.Second} {
|
||||
part := dLeft / div
|
||||
dLeft -= part * div
|
||||
dLeft -= part * div //nolint:durationcheck // One is static, this is fine
|
||||
|
||||
if len(units) <= idx || units[idx] == "" {
|
||||
continue
|
||||
|
|
2
git.go
2
git.go
|
@ -56,6 +56,6 @@ func (g gitHelper) HasRepo() bool {
|
|||
return err == nil
|
||||
}
|
||||
|
||||
func (g gitHelper) getSignature(name, mail string) *object.Signature {
|
||||
func (gitHelper) getSignature(name, mail string) *object.Signature {
|
||||
return &object.Signature{Name: name, Email: mail, When: time.Now()}
|
||||
}
|
||||
|
|
41
go.mod
41
go.mod
|
@ -3,20 +3,20 @@ module github.com/Luzifer/twitch-bot/v3
|
|||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/Luzifer/go-openssl/v4 v4.2.1
|
||||
github.com/Luzifer/go-openssl/v4 v4.2.2
|
||||
github.com/Luzifer/go_helpers/v2 v2.22.0
|
||||
github.com/Luzifer/korvike/functions v0.11.0
|
||||
github.com/Luzifer/rconfig/v2 v2.4.0
|
||||
github.com/Luzifer/rconfig/v2 v2.5.0
|
||||
github.com/Masterminds/sprig/v3 v3.2.3
|
||||
github.com/getsentry/sentry-go v0.25.0
|
||||
github.com/getsentry/sentry-go v0.26.0
|
||||
github.com/glebarez/sqlite v1.10.0
|
||||
github.com/go-git/go-git/v5 v5.10.1
|
||||
github.com/go-git/go-git/v5 v5.11.0
|
||||
github.com/go-sql-driver/mysql v1.7.1
|
||||
github.com/gofrs/uuid v4.4.0+incompatible
|
||||
github.com/gofrs/uuid/v3 v3.1.2
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/itchyny/gojq v0.12.13
|
||||
github.com/itchyny/gojq v0.12.14
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2
|
||||
github.com/orandin/sentrus v1.0.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
|
@ -24,7 +24,8 @@ require (
|
|||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/wzshiming/openapi v0.0.0-20200703171632-c7220b3c9cfb
|
||||
golang.org/x/crypto v0.16.0
|
||||
golang.org/x/crypto v0.18.0
|
||||
golang.org/x/net v0.20.0
|
||||
gopkg.in/irc.v4 v4.0.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/driver/mysql v1.5.2
|
||||
|
@ -37,20 +38,20 @@ require (
|
|||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.2.1 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c // indirect
|
||||
github.com/ProtonMail/go-crypto v1.0.0 // indirect
|
||||
github.com/cenkalti/backoff/v3 v3.2.2 // indirect
|
||||
github.com/cloudflare/circl v1.3.6 // indirect
|
||||
github.com/cloudflare/circl v1.3.7 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/fatih/color v1.14.1 // indirect
|
||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.5.0 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.1 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/google/uuid v1.4.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-hclog v1.4.0 // indirect
|
||||
|
@ -61,13 +62,13 @@ require (
|
|||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
|
||||
github.com/hashicorp/go-sockaddr v1.0.6 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/hashicorp/vault/api v1.10.0 // indirect
|
||||
github.com/hashicorp/vault/api v1.11.0 // indirect
|
||||
github.com/huandu/xstrings v1.4.0 // indirect
|
||||
github.com/imdario/mergo v0.3.16 // indirect
|
||||
github.com/itchyny/timefmt-go v0.1.5 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/pgx/v5 v5.5.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
|
||||
github.com/jackc/pgx/v5 v5.5.2 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
|
@ -86,21 +87,19 @@ require (
|
|||
github.com/sergi/go-diff v1.3.1 // indirect
|
||||
github.com/shopspring/decimal v1.3.1 // indirect
|
||||
github.com/skeema/knownhosts v1.2.1 // indirect
|
||||
github.com/spf13/cast v1.5.1 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
golang.org/x/mod v0.14.0 // indirect
|
||||
golang.org/x/net v0.19.0 // indirect
|
||||
golang.org/x/sync v0.5.0 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
golang.org/x/sync v0.6.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.16.0 // indirect
|
||||
golang.org/x/tools v0.17.0 // indirect
|
||||
gopkg.in/validator.v2 v2.0.1 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
modernc.org/libc v1.34.11 // indirect
|
||||
modernc.org/libc v1.40.7 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.7.2 // indirect
|
||||
modernc.org/sqlite v1.27.0 // indirect
|
||||
modernc.org/sqlite v1.28.0 // indirect
|
||||
)
|
||||
|
|
88
go.sum
88
go.sum
|
@ -2,14 +2,14 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
|
|||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Luzifer/go-openssl/v4 v4.2.1 h1:0+/gaQ5TcBhGmVqGrfyA21eujlbbaNwj0VlOA3nh4ts=
|
||||
github.com/Luzifer/go-openssl/v4 v4.2.1/go.mod h1:CZZZWY0buCtkxrkqDPQYigC4Kn55UuO97TEoV+hwz2s=
|
||||
github.com/Luzifer/go-openssl/v4 v4.2.2 h1:wKF/GhSKGJtHFQYTkN61wXig7mPvDj/oPpW6MmnBpjc=
|
||||
github.com/Luzifer/go-openssl/v4 v4.2.2/go.mod h1:+kAwI4NpyYXoWil85gKSCEJNoCQlMeFikEMn2f+5ffc=
|
||||
github.com/Luzifer/go_helpers/v2 v2.22.0 h1:rJrZkJDzAiq4J0RUbwPI7kQ5rUy7BYQ/GUpo3fSM0y0=
|
||||
github.com/Luzifer/go_helpers/v2 v2.22.0/go.mod h1:cIIqMPu3NT8/6kHke+03hVznNDLLKVGA74Lz47CWJyA=
|
||||
github.com/Luzifer/korvike/functions v0.11.0 h1:2hr3nnt9hy8Esu1W3h50+RggcLRXvrw92kVQLvxzd2Q=
|
||||
github.com/Luzifer/korvike/functions v0.11.0/go.mod h1:osumwH64mWgbwZIfE7rE0BB7Y5HXxrzyO4JfO7fhduU=
|
||||
github.com/Luzifer/rconfig/v2 v2.4.0 h1:MAdymTlExAZ8mx5VG8xOFAtFQSpWBipKYQHPOmYTn9o=
|
||||
github.com/Luzifer/rconfig/v2 v2.4.0/go.mod h1:hWF3ZVSusbYlg5bEvCwalEyUSY+0JPJWUiIu7rBmav8=
|
||||
github.com/Luzifer/rconfig/v2 v2.5.0 h1:zx5lfQbNX3za4VegID97IeY+M+BmfgHxWJTYA94sxok=
|
||||
github.com/Luzifer/rconfig/v2 v2.5.0/go.mod h1:eGWUPQeCPv/Pr/p0hjmwFgI20uqvwi/Szen69hUzGzU=
|
||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
|
@ -20,8 +20,8 @@ github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBa
|
|||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c h1:kMFnB0vCcX7IL/m9Y5LO+KQYv+t1CQOiFe6+SV2J7bE=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
|
||||
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
|
@ -34,8 +34,8 @@ github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTx
|
|||
github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||
github.com/cloudflare/circl v1.3.6 h1:/xbKIqSHbZXHwkhbrhrt2YOHIwYJlXH94E3tI/gDlUg=
|
||||
github.com/cloudflare/circl v1.3.6/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
|
||||
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
|
||||
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
|
||||
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
|
@ -52,12 +52,12 @@ github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYF
|
|||
github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w=
|
||||
github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
|
||||
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/getsentry/sentry-go v0.25.0 h1:q6Eo+hS+yoJlTO3uu/azhQadsD8V+jQn2D8VvX1eOyI=
|
||||
github.com/getsentry/sentry-go v0.25.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
|
||||
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/getsentry/sentry-go v0.26.0 h1:IX3++sF6/4B5JcevhdZfdKIHfyvMmAq/UnqcyT2H6mA=
|
||||
github.com/getsentry/sentry-go v0.26.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
|
||||
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
||||
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
||||
github.com/glebarez/sqlite v1.10.0 h1:u4gt8y7OND/cCei/NMHmfbLxF6xP2wgKcT/BJf2pYkc=
|
||||
github.com/glebarez/sqlite v1.10.0/go.mod h1:IJ+lfSOmiekhQsFTJRx/lHtGYmCdtAiTaf5wI9u5uHA=
|
||||
github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
|
||||
|
@ -70,8 +70,8 @@ github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+
|
|||
github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.10.1 h1:tu8/D8i+TWxgKpzQ3Vc43e+kkhXqtsZCKI/egajKnxk=
|
||||
github.com/go-git/go-git/v5 v5.10.1/go.mod h1:uEuHjxkHap8kAl//V5F/nNWwqIYtP/402ddd05mp0wg=
|
||||
github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4=
|
||||
github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY=
|
||||
github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
|
||||
github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
|
||||
github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
|
||||
|
@ -99,8 +99,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
|
|||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
||||
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
|
@ -143,8 +143,8 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
|
|||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoIospckxBxk6Q=
|
||||
github.com/hashicorp/vault/api v1.10.0 h1:/US7sIjWN6Imp4o/Rj1Ce2Nr5bki/AXi9vAW3p2tOJQ=
|
||||
github.com/hashicorp/vault/api v1.10.0/go.mod h1:jo5Y/ET+hNyz+JnKDt8XLAdKs+AM0G5W0Vp1IrFI8N8=
|
||||
github.com/hashicorp/vault/api v1.11.0 h1:AChWByeHf4/P9sX3Y1B7vFsQhZO2BgQiCMQ2SA1P1UY=
|
||||
github.com/hashicorp/vault/api v1.11.0/go.mod h1:si+lJCYO7oGkIoNPAN8j3azBLTn9SjMGS+jFaHd1Cck=
|
||||
github.com/hashicorp/vault/sdk v0.1.13/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M=
|
||||
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
|
||||
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
|
||||
|
@ -154,16 +154,16 @@ github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq
|
|||
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
|
||||
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
||||
github.com/itchyny/gojq v0.12.13 h1:IxyYlHYIlspQHHTE0f3cJF0NKDMfajxViuhBLnHd/QU=
|
||||
github.com/itchyny/gojq v0.12.13/go.mod h1:JzwzAqenfhrPUuwbmEz3nu3JQmFLlQTQMUcOdnu/Sf4=
|
||||
github.com/itchyny/gojq v0.12.14 h1:6k8vVtsrhQSYgSGg827AD+PVVaB1NLXEdX+dda2oZCc=
|
||||
github.com/itchyny/gojq v0.12.14/go.mod h1:y1G7oO7XkcR1LPZO59KyoCRy08T3j9vDYRV0GgYSS+s=
|
||||
github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE=
|
||||
github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.5.0 h1:NxstgwndsTRy7eq9/kqYc/BZh5w2hHJV86wjvO+1xPw=
|
||||
github.com/jackc/pgx/v5 v5.5.0/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
|
||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
|
||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.5.2 h1:iLlpgp4Cp/gC9Xuscl7lFL1PhhW+ZLtXZcrfCt4C3tA=
|
||||
github.com/jackc/pgx/v5 v5.5.2/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
|
||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
|
@ -248,8 +248,8 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs
|
|||
github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ=
|
||||
github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
|
||||
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
|
@ -277,8 +277,8 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0
|
|||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
|
||||
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
|
@ -299,8 +299,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
|||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
@ -309,8 +309,8 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
|
|||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
@ -334,15 +334,15 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
|
||||
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
|
@ -365,8 +365,8 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn
|
|||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM=
|
||||
golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
|
||||
golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
|
||||
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
|
@ -404,11 +404,11 @@ gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
|
|||
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
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=
|
||||
modernc.org/libc v1.34.11 h1:hQDcIUlSG4QAOkXCIQKkaAOV5ptXvkOx4ddbXzgW2JU=
|
||||
modernc.org/libc v1.34.11/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
|
||||
modernc.org/libc v1.40.7 h1:oeLS0G067ZqUu+v143Dqad0btMfKmNS7SuOsnkq0Ysg=
|
||||
modernc.org/libc v1.40.7/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
|
||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
|
||||
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
|
||||
modernc.org/sqlite v1.27.0 h1:MpKAHoyYB7xqcwnUwkuD+npwEa0fojF0B5QRbN+auJ8=
|
||||
modernc.org/sqlite v1.27.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
|
||||
modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
|
||||
modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
// Package announce contains a chat essage handler to create
|
||||
// announcements from the bot
|
||||
package announce
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
@ -16,6 +19,7 @@ var (
|
|||
announceChatcommandRegex = regexp.MustCompile(`^/announce(|blue|green|orange|purple) +(.+)$`)
|
||||
)
|
||||
|
||||
// Register provides the plugins.RegisterFunc
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
botTwitchClient = args.GetTwitchClient()
|
||||
|
||||
|
@ -32,7 +36,7 @@ func handleChatCommand(m *irc.Message) error {
|
|||
return errors.New("announce message does not match required format")
|
||||
}
|
||||
|
||||
if err := botTwitchClient.SendChatAnnouncement(channel, matches[1], matches[2]); err != nil {
|
||||
if err := botTwitchClient.SendChatAnnouncement(context.Background(), channel, matches[1], matches[2]); err != nil {
|
||||
return errors.Wrap(err, "sending announcement")
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
// Package ban contains actors to ban/unban users in a channel
|
||||
package ban
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
|
@ -21,7 +24,8 @@ var (
|
|||
banChatcommandRegex = regexp.MustCompile(`^/ban +([^\s]+) +(.+)$`)
|
||||
)
|
||||
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
// Register provides the plugins.RegisterFunc
|
||||
func Register(args plugins.RegistrationArguments) (err error) {
|
||||
botTwitchClient = args.GetTwitchClient()
|
||||
formatMessage = args.FormatMessage
|
||||
|
||||
|
@ -45,7 +49,7 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
},
|
||||
})
|
||||
|
||||
args.RegisterAPIRoute(plugins.HTTPRouteRegistrationArgs{
|
||||
if err = args.RegisterAPIRoute(plugins.HTTPRouteRegistrationArgs{
|
||||
Description: "Executes a ban of an user in the specified channel",
|
||||
HandlerFunc: handleAPIBan,
|
||||
Method: http.MethodPost,
|
||||
|
@ -72,7 +76,9 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
Name: "user",
|
||||
},
|
||||
},
|
||||
})
|
||||
}); err != nil {
|
||||
return fmt.Errorf("registering API route: %w", err)
|
||||
}
|
||||
|
||||
args.RegisterMessageModFunc("/ban", handleChatCommand)
|
||||
|
||||
|
@ -81,7 +87,7 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
|
||||
type actor struct{}
|
||||
|
||||
func (a actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
ptrStringEmpty := func(v string) *string { return &v }("")
|
||||
|
||||
reason, err := formatMessage(attrs.MustString("reason", ptrStringEmpty), m, r, eventData)
|
||||
|
@ -91,6 +97,7 @@ func (a actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData
|
|||
|
||||
return false, errors.Wrap(
|
||||
botTwitchClient.BanUser(
|
||||
context.Background(),
|
||||
plugins.DeriveChannel(m, eventData),
|
||||
plugins.DeriveUser(m, eventData),
|
||||
0,
|
||||
|
@ -100,10 +107,10 @@ func (a actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData
|
|||
)
|
||||
}
|
||||
|
||||
func (a actor) IsAsync() bool { return false }
|
||||
func (a actor) Name() string { return actorName }
|
||||
func (actor) IsAsync() bool { return false }
|
||||
func (actor) Name() string { return actorName }
|
||||
|
||||
func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
reasonTemplate, err := attrs.String("reason")
|
||||
if err != nil || reasonTemplate == "" {
|
||||
return errors.New("reason must be non-empty string")
|
||||
|
@ -124,7 +131,7 @@ func handleAPIBan(w http.ResponseWriter, r *http.Request) {
|
|||
reason = r.FormValue("reason")
|
||||
)
|
||||
|
||||
if err := botTwitchClient.BanUser(channel, user, 0, reason); err != nil {
|
||||
if err := botTwitchClient.BanUser(r.Context(), channel, user, 0, reason); err != nil {
|
||||
http.Error(w, errors.Wrap(err, "issuing ban").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
@ -140,7 +147,7 @@ func handleChatCommand(m *irc.Message) error {
|
|||
return errors.New("ban message does not match required format")
|
||||
}
|
||||
|
||||
if err := botTwitchClient.BanUser(channel, matches[1], 0, matches[2]); err != nil {
|
||||
if err := botTwitchClient.BanUser(context.Background(), channel, matches[1], 0, matches[2]); err != nil {
|
||||
return errors.Wrap(err, "executing ban")
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// Package clip contains an actor to create clips on behalf of a
|
||||
// channels owner
|
||||
package clip
|
||||
|
||||
import (
|
||||
|
@ -22,6 +24,7 @@ var (
|
|||
ptrStringEmpty = func(s string) *string { return &s }("")
|
||||
)
|
||||
|
||||
// Register provides the plugins.RegisterFunc
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
formatMessage = args.FormatMessage
|
||||
hasPerm = args.HasPermissionForChannel
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// Package clipdetector contains an actor to detect clip links in a
|
||||
// message and populate a template variable
|
||||
package clipdetector
|
||||
|
||||
import (
|
||||
|
@ -19,6 +21,7 @@ var (
|
|||
clipIDScanner = regexp.MustCompile(`(?:clips\.twitch\.tv|www\.twitch\.tv/[^/]*/clip)/([A-Za-z0-9_-]+)`)
|
||||
)
|
||||
|
||||
// Register provides the plugins.RegisterFunc
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
botTwitchClient = args.GetTwitchClient()
|
||||
|
||||
|
@ -33,8 +36,10 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Actor implements the actor interface
|
||||
type Actor struct{}
|
||||
|
||||
// Execute implements the actor interface
|
||||
func (Actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
if eventData.HasAll("clips") {
|
||||
// We already detected clips, lets not do it again
|
||||
|
@ -70,8 +75,11 @@ func (Actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData *
|
|||
return false, nil
|
||||
}
|
||||
|
||||
// IsAsync implements the actor interface
|
||||
func (Actor) IsAsync() bool { return false }
|
||||
|
||||
// Name implements the actor interface
|
||||
func (Actor) Name() string { return actorName }
|
||||
|
||||
// Validate implements the actor interface
|
||||
func (Actor) Validate(plugins.TemplateValidatorFunc, *plugins.FieldCollection) error { return nil }
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// Package commercial contains an actor to run commercials in a channel
|
||||
package commercial
|
||||
|
||||
import (
|
||||
|
@ -27,6 +28,7 @@ var (
|
|||
commercialChatcommandRegex = regexp.MustCompile(`^/commercial ([0-9]+)$`)
|
||||
)
|
||||
|
||||
// Register provides the plugins.RegisterFunc
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
formatMessage = args.FormatMessage
|
||||
permCheckFn = args.HasPermissionForChannel
|
||||
|
@ -70,10 +72,10 @@ func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *
|
|||
return false, startCommercial(strings.TrimLeft(plugins.DeriveChannel(m, eventData), "#"), durationStr)
|
||||
}
|
||||
|
||||
func (a actor) IsAsync() bool { return false }
|
||||
func (a actor) Name() string { return actorName }
|
||||
func (actor) IsAsync() bool { return false }
|
||||
func (actor) Name() string { return actorName }
|
||||
|
||||
func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
durationTemplate, err := attrs.String("duration")
|
||||
if err != nil || durationTemplate == "" {
|
||||
return errors.New("duration must be non-empty string")
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// Package counter contains actors and template functions to work with
|
||||
// database stored counters
|
||||
package counter
|
||||
|
||||
import (
|
||||
|
@ -22,20 +24,22 @@ var (
|
|||
ptrStringEmpty = func(s string) *string { return &s }("")
|
||||
)
|
||||
|
||||
// Register provides the plugins.RegisterFunc
|
||||
//
|
||||
//nolint:funlen // This function is a few lines too long but only contains definitions
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
func Register(args plugins.RegistrationArguments) (err error) {
|
||||
db = args.GetDatabaseConnector()
|
||||
if err := db.DB().AutoMigrate(&Counter{}); err != nil {
|
||||
if err = db.DB().AutoMigrate(&counter{}); err != nil {
|
||||
return errors.Wrap(err, "applying schema migration")
|
||||
}
|
||||
|
||||
args.RegisterCopyDatabaseFunc("counter", func(src, target *gorm.DB) error {
|
||||
return database.CopyObjects(src, target, &Counter{})
|
||||
return database.CopyObjects(src, target, &counter{}) //nolint:wrapcheck // internal helper
|
||||
})
|
||||
|
||||
formatMessage = args.FormatMessage
|
||||
|
||||
args.RegisterActor("counter", func() plugins.Actor { return &ActorCounter{} })
|
||||
args.RegisterActor("counter", func() plugins.Actor { return &actorCounter{} })
|
||||
|
||||
args.RegisterActorDocumentation(plugins.ActionDocumentation{
|
||||
Description: "Update counter values",
|
||||
|
@ -73,7 +77,7 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
},
|
||||
})
|
||||
|
||||
args.RegisterAPIRoute(plugins.HTTPRouteRegistrationArgs{
|
||||
if err = args.RegisterAPIRoute(plugins.HTTPRouteRegistrationArgs{
|
||||
Description: "Returns the (formatted) value as a plain string",
|
||||
HandlerFunc: routeActorCounterGetValue,
|
||||
Method: http.MethodGet,
|
||||
|
@ -95,9 +99,11 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
Name: "name",
|
||||
},
|
||||
},
|
||||
})
|
||||
}); err != nil {
|
||||
return fmt.Errorf("registering API route: %w", err)
|
||||
}
|
||||
|
||||
args.RegisterAPIRoute(plugins.HTTPRouteRegistrationArgs{
|
||||
if err = args.RegisterAPIRoute(plugins.HTTPRouteRegistrationArgs{
|
||||
Description: "Updates the value of the counter",
|
||||
HandlerFunc: routeActorCounterSetValue,
|
||||
Method: http.MethodPatch,
|
||||
|
@ -125,7 +131,9 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
Name: "name",
|
||||
},
|
||||
},
|
||||
})
|
||||
}); err != nil {
|
||||
return fmt.Errorf("registering API route: %w", err)
|
||||
}
|
||||
|
||||
args.RegisterTemplateFunction("channelCounter", func(m *irc.Message, r *plugins.Rule, fields *plugins.FieldCollection) interface{} {
|
||||
return func(name string) (string, error) {
|
||||
|
@ -157,7 +165,7 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
},
|
||||
})
|
||||
|
||||
args.RegisterTemplateFunction("counterTopList", plugins.GenericTemplateFunctionGetter(func(prefix string, n int) ([]Counter, error) {
|
||||
args.RegisterTemplateFunction("counterTopList", plugins.GenericTemplateFunctionGetter(func(prefix string, n int) ([]counter, error) {
|
||||
return getCounterTopList(db, prefix, n)
|
||||
}), plugins.TemplateFuncDocumentation{
|
||||
Description: "Returns the top n counters for the given prefix as objects with Name and Value fields",
|
||||
|
@ -169,7 +177,7 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
})
|
||||
|
||||
args.RegisterTemplateFunction("counterValue", plugins.GenericTemplateFunctionGetter(func(name string, _ ...string) (int64, error) {
|
||||
return GetCounterValue(db, name)
|
||||
return getCounterValue(db, name)
|
||||
}), plugins.TemplateFuncDocumentation{
|
||||
Description: "Returns the current value of the counter which identifier was supplied",
|
||||
Syntax: "counterValue <counter name>",
|
||||
|
@ -185,11 +193,11 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
mod = val[0]
|
||||
}
|
||||
|
||||
if err := UpdateCounter(db, name, mod, false); err != nil {
|
||||
if err := updateCounter(db, name, mod, false); err != nil {
|
||||
return 0, errors.Wrap(err, "updating counter")
|
||||
}
|
||||
|
||||
return GetCounterValue(db, name)
|
||||
return getCounterValue(db, name)
|
||||
}), plugins.TemplateFuncDocumentation{
|
||||
Description: "Adds the given value (or 1 if no value) to the counter and returns its new value",
|
||||
Syntax: "counterValueAdd <counter name> [increase=1]",
|
||||
|
@ -202,9 +210,9 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
type ActorCounter struct{}
|
||||
type actorCounter struct{}
|
||||
|
||||
func (a ActorCounter) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
func (actorCounter) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
counterName, err := formatMessage(attrs.MustString("counter", nil), m, r, eventData)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "preparing response")
|
||||
|
@ -222,7 +230,7 @@ func (a ActorCounter) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, ev
|
|||
}
|
||||
|
||||
return false, errors.Wrap(
|
||||
UpdateCounter(db, counterName, counterValue, true),
|
||||
updateCounter(db, counterName, counterValue, true),
|
||||
"set counter",
|
||||
)
|
||||
}
|
||||
|
@ -241,15 +249,15 @@ func (a ActorCounter) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, ev
|
|||
}
|
||||
|
||||
return false, errors.Wrap(
|
||||
UpdateCounter(db, counterName, counterStep, false),
|
||||
updateCounter(db, counterName, counterStep, false),
|
||||
"update counter",
|
||||
)
|
||||
}
|
||||
|
||||
func (a ActorCounter) IsAsync() bool { return false }
|
||||
func (a ActorCounter) Name() string { return "counter" }
|
||||
func (actorCounter) IsAsync() bool { return false }
|
||||
func (actorCounter) Name() string { return "counter" }
|
||||
|
||||
func (a ActorCounter) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
func (actorCounter) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
if cn, err := attrs.String("counter"); err != nil || cn == "" {
|
||||
return errors.New("counter name must be non-empty string")
|
||||
}
|
||||
|
@ -269,7 +277,7 @@ func routeActorCounterGetValue(w http.ResponseWriter, r *http.Request) {
|
|||
template = "%d"
|
||||
}
|
||||
|
||||
cv, err := GetCounterValue(db, mux.Vars(r)["name"])
|
||||
cv, err := getCounterValue(db, mux.Vars(r)["name"])
|
||||
if err != nil {
|
||||
http.Error(w, errors.Wrap(err, "getting value").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
@ -291,7 +299,7 @@ func routeActorCounterSetValue(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if err = UpdateCounter(db, mux.Vars(r)["name"], value, absolute); err != nil {
|
||||
if err = updateCounter(db, mux.Vars(r)["name"], value, absolute); err != nil {
|
||||
http.Error(w, errors.Wrap(err, "updating value").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -10,14 +10,14 @@ import (
|
|||
)
|
||||
|
||||
type (
|
||||
Counter struct {
|
||||
counter struct {
|
||||
Name string `gorm:"primaryKey"`
|
||||
Value int64
|
||||
}
|
||||
)
|
||||
|
||||
func GetCounterValue(db database.Connector, counterName string) (int64, error) {
|
||||
var c Counter
|
||||
func getCounterValue(db database.Connector, counterName string) (int64, error) {
|
||||
var c counter
|
||||
|
||||
err := helpers.Retry(func() error {
|
||||
err := db.DB().First(&c, "name = ?", counterName).Error
|
||||
|
@ -31,9 +31,10 @@ func GetCounterValue(db database.Connector, counterName string) (int64, error) {
|
|||
return c.Value, errors.Wrap(err, "querying counter")
|
||||
}
|
||||
|
||||
func UpdateCounter(db database.Connector, counterName string, value int64, absolute bool) error {
|
||||
//revive:disable-next-line:flag-parameter
|
||||
func updateCounter(db database.Connector, counterName string, value int64, absolute bool) error {
|
||||
if !absolute {
|
||||
cv, err := GetCounterValue(db, counterName)
|
||||
cv, err := getCounterValue(db, counterName)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "getting previous value")
|
||||
}
|
||||
|
@ -46,14 +47,14 @@ func UpdateCounter(db database.Connector, counterName string, value int64, absol
|
|||
return tx.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "name"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"value"}),
|
||||
}).Create(Counter{Name: counterName, Value: value}).Error
|
||||
}).Create(counter{Name: counterName, Value: value}).Error
|
||||
}),
|
||||
"storing counter value",
|
||||
)
|
||||
}
|
||||
|
||||
func getCounterRank(db database.Connector, prefix, name string) (rank, count int64, err error) {
|
||||
var cc []Counter
|
||||
var cc []counter
|
||||
|
||||
if err = helpers.Retry(func() error {
|
||||
return db.DB().
|
||||
|
@ -74,8 +75,8 @@ func getCounterRank(db database.Connector, prefix, name string) (rank, count int
|
|||
return rank, count, nil
|
||||
}
|
||||
|
||||
func getCounterTopList(db database.Connector, prefix string, n int) ([]Counter, error) {
|
||||
var cc []Counter
|
||||
func getCounterTopList(db database.Connector, prefix string, n int) ([]counter, error) {
|
||||
var cc []counter
|
||||
|
||||
err := helpers.Retry(func() error {
|
||||
return db.DB().
|
||||
|
|
|
@ -12,34 +12,34 @@ import (
|
|||
|
||||
func TestCounterStoreLoop(t *testing.T) {
|
||||
dbc := database.GetTestDatabase(t)
|
||||
dbc.DB().AutoMigrate(&Counter{})
|
||||
require.NoError(t, dbc.DB().AutoMigrate(&counter{}))
|
||||
|
||||
counterName := "mytestcounter"
|
||||
|
||||
v, err := GetCounterValue(dbc, counterName)
|
||||
v, err := getCounterValue(dbc, counterName)
|
||||
assert.NoError(t, err, "reading non-existent counter")
|
||||
assert.Equal(t, int64(0), v, "expecting 0 counter value on non-existent counter")
|
||||
|
||||
err = UpdateCounter(dbc, counterName, 5, true)
|
||||
err = updateCounter(dbc, counterName, 5, true)
|
||||
assert.NoError(t, err, "inserting counter")
|
||||
|
||||
err = UpdateCounter(dbc, counterName, 1, false)
|
||||
err = updateCounter(dbc, counterName, 1, false)
|
||||
assert.NoError(t, err, "updating counter")
|
||||
|
||||
v, err = GetCounterValue(dbc, counterName)
|
||||
v, err = getCounterValue(dbc, counterName)
|
||||
assert.NoError(t, err, "reading existent counter")
|
||||
assert.Equal(t, int64(6), v, "expecting counter value on existing counter")
|
||||
}
|
||||
|
||||
func TestCounterTopListAndRank(t *testing.T) {
|
||||
dbc := database.GetTestDatabase(t)
|
||||
dbc.DB().AutoMigrate(&Counter{})
|
||||
require.NoError(t, dbc.DB().AutoMigrate(&counter{}))
|
||||
|
||||
counterTemplate := `#example:test:%v`
|
||||
for i := 0; i < 6; i++ {
|
||||
require.NoError(
|
||||
t,
|
||||
UpdateCounter(dbc, fmt.Sprintf(counterTemplate, i), int64(i), true),
|
||||
updateCounter(dbc, fmt.Sprintf(counterTemplate, i), int64(i), true),
|
||||
"inserting counter %d", i,
|
||||
)
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ func TestCounterTopListAndRank(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
assert.Len(t, cc, 3)
|
||||
|
||||
assert.Equal(t, []Counter{
|
||||
assert.Equal(t, []counter{
|
||||
{Name: "#example:test:5", Value: 5},
|
||||
{Name: "#example:test:4", Value: 4},
|
||||
{Name: "#example:test:3", Value: 3},
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// Package delay contains an actor to delay rule execution
|
||||
package delay
|
||||
|
||||
import (
|
||||
|
@ -11,6 +12,7 @@ import (
|
|||
|
||||
const actorName = "delay"
|
||||
|
||||
// Register provides the plugins.RegisterFunc
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
args.RegisterActor(actorName, func() plugins.Actor { return &actor{} })
|
||||
|
||||
|
@ -46,7 +48,7 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
|
||||
type actor struct{}
|
||||
|
||||
func (a actor) Execute(_ *irc.Client, _ *irc.Message, _ *plugins.Rule, _ *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
func (actor) Execute(_ *irc.Client, _ *irc.Message, _ *plugins.Rule, _ *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
var (
|
||||
ptrZeroDuration = func(v time.Duration) *time.Duration { return &v }(0)
|
||||
delay = attrs.MustDuration("delay", ptrZeroDuration)
|
||||
|
@ -66,9 +68,9 @@ func (a actor) Execute(_ *irc.Client, _ *irc.Message, _ *plugins.Rule, _ *plugin
|
|||
return false, nil
|
||||
}
|
||||
|
||||
func (a actor) IsAsync() bool { return false }
|
||||
func (a actor) Name() string { return actorName }
|
||||
func (actor) IsAsync() bool { return false }
|
||||
func (actor) Name() string { return actorName }
|
||||
|
||||
func (a actor) Validate(plugins.TemplateValidatorFunc, *plugins.FieldCollection) (err error) {
|
||||
func (actor) Validate(plugins.TemplateValidatorFunc, *plugins.FieldCollection) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
// Package deleteactor contains an actor to delete messages
|
||||
package deleteactor
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/irc.v4"
|
||||
|
||||
|
@ -12,6 +15,7 @@ const actorName = "delete"
|
|||
|
||||
var botTwitchClient *twitch.Client
|
||||
|
||||
// Register provides the plugins.RegisterFunc
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
botTwitchClient = args.GetTwitchClient()
|
||||
|
||||
|
@ -28,7 +32,7 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
|
||||
type actor struct{}
|
||||
|
||||
func (a actor) Execute(_ *irc.Client, m *irc.Message, _ *plugins.Rule, eventData *plugins.FieldCollection, _ *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
func (actor) Execute(_ *irc.Client, m *irc.Message, _ *plugins.Rule, eventData *plugins.FieldCollection, _ *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
msgID, ok := m.Tags["id"]
|
||||
if !ok || msgID == "" {
|
||||
return false, nil
|
||||
|
@ -36,6 +40,7 @@ func (a actor) Execute(_ *irc.Client, m *irc.Message, _ *plugins.Rule, eventData
|
|||
|
||||
return false, errors.Wrap(
|
||||
botTwitchClient.DeleteMessage(
|
||||
context.Background(),
|
||||
plugins.DeriveChannel(m, eventData),
|
||||
msgID,
|
||||
),
|
||||
|
@ -43,9 +48,9 @@ func (a actor) Execute(_ *irc.Client, m *irc.Message, _ *plugins.Rule, eventData
|
|||
)
|
||||
}
|
||||
|
||||
func (a actor) IsAsync() bool { return false }
|
||||
func (a actor) Name() string { return actorName }
|
||||
func (actor) IsAsync() bool { return false }
|
||||
func (actor) Name() string { return actorName }
|
||||
|
||||
func (a actor) Validate(plugins.TemplateValidatorFunc, *plugins.FieldCollection) (err error) {
|
||||
func (actor) Validate(plugins.TemplateValidatorFunc, *plugins.FieldCollection) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// Package eventmod contains an actor to modify event data during rule
|
||||
// execution by adding fields (template variables)
|
||||
package eventmod
|
||||
|
||||
import (
|
||||
|
@ -13,6 +15,7 @@ const actorName = "eventmod"
|
|||
|
||||
var formatMessage plugins.MsgFormatter
|
||||
|
||||
// Register provides the plugins.RegisterFunc
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
formatMessage = args.FormatMessage
|
||||
|
||||
|
@ -41,7 +44,7 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
|
||||
type actor struct{}
|
||||
|
||||
func (a actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
ptrStringEmpty := func(v string) *string { return &v }("")
|
||||
|
||||
fd, err := formatMessage(attrs.MustString("fields", ptrStringEmpty), m, r, eventData)
|
||||
|
@ -63,10 +66,10 @@ func (a actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData
|
|||
return false, nil
|
||||
}
|
||||
|
||||
func (a actor) IsAsync() bool { return false }
|
||||
func (a actor) Name() string { return actorName }
|
||||
func (actor) IsAsync() bool { return false }
|
||||
func (actor) Name() string { return actorName }
|
||||
|
||||
func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
fieldsTemplate, err := attrs.String("fields")
|
||||
if err != nil || fieldsTemplate == "" {
|
||||
return errors.New("fields must be non-empty string")
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// Package filesay contains an actor to paste a remote URL as chat
|
||||
// commands i.e. for bulk banning users
|
||||
package filesay
|
||||
|
||||
import (
|
||||
|
@ -8,6 +10,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/irc.v4"
|
||||
|
||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||
|
@ -24,6 +27,7 @@ var (
|
|||
send plugins.SendMessageFunc
|
||||
)
|
||||
|
||||
// Register provides the plugins.RegisterFunc
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
formatMessage = args.FormatMessage
|
||||
send = args.SendMessage
|
||||
|
@ -53,7 +57,7 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
|
||||
type actor struct{}
|
||||
|
||||
func (a actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
ptrStringEmpty := func(v string) *string { return &v }("")
|
||||
|
||||
source, err := formatMessage(attrs.MustString("source", ptrStringEmpty), m, r, eventData)
|
||||
|
@ -81,7 +85,11 @@ func (a actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData
|
|||
if err != nil {
|
||||
return false, errors.Wrap(err, "executing HTTP request")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
logrus.WithError(err).Error("closing response body (leaked fd)")
|
||||
}
|
||||
}()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return false, errors.Errorf("http status %d", resp.StatusCode)
|
||||
|
@ -103,10 +111,10 @@ func (a actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData
|
|||
return false, nil
|
||||
}
|
||||
|
||||
func (a actor) IsAsync() bool { return true }
|
||||
func (a actor) Name() string { return actorName }
|
||||
func (actor) IsAsync() bool { return true }
|
||||
func (actor) Name() string { return actorName }
|
||||
|
||||
func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) error {
|
||||
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) error {
|
||||
sourceTpl, err := attrs.String("source")
|
||||
if err != nil || sourceTpl == "" {
|
||||
return errors.New("source is expected to be non-empty string")
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// Package linkdetector contains an actor to detect links in a message
|
||||
// and add them to a variable
|
||||
package linkdetector
|
||||
|
||||
import (
|
||||
|
@ -11,6 +13,7 @@ const actorName = "linkdetector"
|
|||
|
||||
var ptrFalse = func(v bool) *bool { return &v }(false)
|
||||
|
||||
// Register provides the plugins.RegisterFunc
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
args.RegisterActor(actorName, func() plugins.Actor { return &Actor{} })
|
||||
|
||||
|
@ -35,8 +38,10 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Actor implements the actor interface
|
||||
type Actor struct{}
|
||||
|
||||
// Execute implements the actor interface
|
||||
func (Actor) Execute(_ *irc.Client, m *irc.Message, _ *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
if eventData.HasAll("links") {
|
||||
// We already detected links, lets not do it again
|
||||
|
@ -52,8 +57,11 @@ func (Actor) Execute(_ *irc.Client, m *irc.Message, _ *plugins.Rule, eventData *
|
|||
return false, nil
|
||||
}
|
||||
|
||||
// IsAsync implements the actor interface
|
||||
func (Actor) IsAsync() bool { return false }
|
||||
|
||||
// Name implements the actor interface
|
||||
func (Actor) Name() string { return actorName }
|
||||
|
||||
// Validate implements the actor interface
|
||||
func (Actor) Validate(plugins.TemplateValidatorFunc, *plugins.FieldCollection) error { return nil }
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
// Package linkprotect contains an actor to prevent chatters from
|
||||
// posting certain links
|
||||
package linkprotect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -22,6 +25,7 @@ var (
|
|||
ptrStringEmpty = func(v string) *string { return &v }("")
|
||||
)
|
||||
|
||||
// Register provides the plugins.RegisterFunc
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
botTwitchClient = args.GetTwitchClient()
|
||||
|
||||
|
@ -163,6 +167,7 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
|
|||
switch lt := attrs.MustString("action", ptrStringEmpty); lt {
|
||||
case "ban":
|
||||
if err = botTwitchClient.BanUser(
|
||||
context.Background(),
|
||||
plugins.DeriveChannel(m, eventData),
|
||||
strings.TrimLeft(plugins.DeriveUser(m, eventData), "@"),
|
||||
0,
|
||||
|
@ -178,6 +183,7 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
|
|||
}
|
||||
|
||||
if err = botTwitchClient.DeleteMessage(
|
||||
context.Background(),
|
||||
plugins.DeriveChannel(m, eventData),
|
||||
msgID,
|
||||
); err != nil {
|
||||
|
@ -191,6 +197,7 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
|
|||
}
|
||||
|
||||
if err = botTwitchClient.BanUser(
|
||||
context.Background(),
|
||||
plugins.DeriveChannel(m, eventData),
|
||||
strings.TrimLeft(plugins.DeriveUser(m, eventData), "@"),
|
||||
to,
|
||||
|
@ -291,6 +298,7 @@ func (actor) checkClipChannelDenied(denyList []string, clips []twitch.ClipInfo)
|
|||
return verdictAllFine
|
||||
}
|
||||
|
||||
//revive:disable-next-line:flag-parameter
|
||||
func (actor) checkAllLinksAllowed(allowList, links []string, autoAllowClipLinks bool) verdict {
|
||||
if len(allowList) == 0 {
|
||||
// We're not explicitly allowing links, this method is a no-op
|
||||
|
@ -322,6 +330,7 @@ func (actor) checkAllLinksAllowed(allowList, links []string, autoAllowClipLinks
|
|||
return verdictMisbehave
|
||||
}
|
||||
|
||||
//revive:disable-next-line:flag-parameter
|
||||
func (actor) checkLinkDenied(denyList, links []string, ignoreClipLinks bool) verdict {
|
||||
for _, link := range links {
|
||||
if ignoreClipLinks && clipLink.MatchString(link) {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// Package log contains an actor to write bot-log entries from a rule
|
||||
package log
|
||||
|
||||
import (
|
||||
|
@ -14,6 +15,7 @@ var (
|
|||
ptrStringEmpty = func(v string) *string { return &v }("")
|
||||
)
|
||||
|
||||
// Register provides the plugins.RegisterFunc
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
formatMessage = args.FormatMessage
|
||||
|
||||
|
@ -42,7 +44,7 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
|
||||
type actor struct{}
|
||||
|
||||
func (a actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
message, err := formatMessage(attrs.MustString("message", ptrStringEmpty), m, r, eventData)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "executing message template")
|
||||
|
@ -56,10 +58,10 @@ func (a actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData
|
|||
return false, nil
|
||||
}
|
||||
|
||||
func (a actor) IsAsync() bool { return true }
|
||||
func (a actor) Name() string { return "log" }
|
||||
func (actor) IsAsync() bool { return true }
|
||||
func (actor) Name() string { return "log" }
|
||||
|
||||
func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
if v, err := attrs.String("message"); err != nil || v == "" {
|
||||
return errors.New("message must be non-empty string")
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// Package messagehook contains actors to send discord / slack webhook
|
||||
// requests
|
||||
package messagehook
|
||||
|
||||
import (
|
||||
|
@ -25,6 +27,7 @@ var (
|
|||
ptrStringEmpty = func(s string) *string { return &s }("")
|
||||
)
|
||||
|
||||
// Register provides the plugins.RegisterFunc
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
formatMessage = args.FormatMessage
|
||||
|
||||
|
@ -55,7 +58,11 @@ func sendPayload(hookURL string, payload any, expRespCode int) (preventCooldown
|
|||
if err != nil {
|
||||
return false, errors.Wrap(err, "executing request")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
logrus.WithError(err).Error("closing response body (leaked fd)")
|
||||
}
|
||||
}()
|
||||
|
||||
if resp.StatusCode != expRespCode {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
|
|
|
@ -78,23 +78,24 @@ func (discordActor) Name() string { return "discordhook" }
|
|||
|
||||
func (d discordActor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
if err = d.ValidateRequireNonEmpty(attrs, "hook_url"); err != nil {
|
||||
return err
|
||||
return err //nolint:wrapcheck
|
||||
}
|
||||
|
||||
if err = d.ValidateRequireValidTemplate(tplValidator, attrs, "content"); err != nil {
|
||||
return err
|
||||
return err //nolint:wrapcheck
|
||||
}
|
||||
|
||||
if err = d.ValidateRequireValidTemplateIfSet(tplValidator, attrs, "avatar_url", "username"); err != nil {
|
||||
return err
|
||||
return err //nolint:wrapcheck
|
||||
}
|
||||
|
||||
if !attrs.MustBool("add_embed", ptrBoolFalse) {
|
||||
// We're not validating the rest if embeds are disabled but in
|
||||
// this case the content is mandatory
|
||||
return d.ValidateRequireNonEmpty(attrs, "content")
|
||||
return d.ValidateRequireNonEmpty(attrs, "content") //nolint:wrapcheck
|
||||
}
|
||||
|
||||
//nolint:wrapcheck
|
||||
return d.ValidateRequireValidTemplateIfSet(
|
||||
tplValidator, attrs,
|
||||
"embed_title",
|
||||
|
|
|
@ -35,9 +35,10 @@ func (slackCompatibleActor) Name() string { return "slackhook" }
|
|||
|
||||
func (s slackCompatibleActor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
if err = s.ValidateRequireNonEmpty(attrs, "hook_url", "text"); err != nil {
|
||||
return err
|
||||
return err //nolint:wrapcheck
|
||||
}
|
||||
|
||||
//nolint:wrapcheck
|
||||
return s.ValidateRequireValidTemplate(tplValidator, attrs, "text")
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// Package modchannel contains an actor to modify title / category of
|
||||
// a channel
|
||||
package modchannel
|
||||
|
||||
import (
|
||||
|
@ -20,6 +22,7 @@ var (
|
|||
ptrStringEmpty = func(s string) *string { return &s }("")
|
||||
)
|
||||
|
||||
// Register provides the plugins.RegisterFunc
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
formatMessage = args.FormatMessage
|
||||
tcGetter = args.GetTwitchClientForChannel
|
||||
|
@ -67,7 +70,7 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
|
||||
type actor struct{}
|
||||
|
||||
func (a actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
var (
|
||||
game = attrs.MustString("game", ptrStringEmpty)
|
||||
title = attrs.MustString("title", ptrStringEmpty)
|
||||
|
@ -113,10 +116,10 @@ func (a actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData
|
|||
)
|
||||
}
|
||||
|
||||
func (a actor) IsAsync() bool { return false }
|
||||
func (a actor) Name() string { return actorName }
|
||||
func (actor) IsAsync() bool { return false }
|
||||
func (actor) Name() string { return actorName }
|
||||
|
||||
func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
if v, err := attrs.String("channel"); err != nil || v == "" {
|
||||
return errors.New("channel must be non-empty string")
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package nuke
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
|
@ -14,6 +15,7 @@ type (
|
|||
func actionBan(channel, match, _, user string) error {
|
||||
return errors.Wrap(
|
||||
botTwitchClient.BanUser(
|
||||
context.Background(),
|
||||
channel,
|
||||
user,
|
||||
0,
|
||||
|
@ -26,6 +28,7 @@ func actionBan(channel, match, _, user string) error {
|
|||
func actionDelete(channel, _, msgid, _ string) (err error) {
|
||||
return errors.Wrap(
|
||||
botTwitchClient.DeleteMessage(
|
||||
context.Background(),
|
||||
channel,
|
||||
msgid,
|
||||
),
|
||||
|
@ -37,6 +40,7 @@ func getActionTimeout(duration time.Duration) actionFn {
|
|||
return func(channel, match, msgid, user string) error {
|
||||
return errors.Wrap(
|
||||
botTwitchClient.BanUser(
|
||||
context.Background(),
|
||||
channel,
|
||||
user,
|
||||
duration,
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
// Package nuke contains a hateraid protection actor recording messages
|
||||
// in all channels for a certain period of time being able to "nuke"
|
||||
// their authors by regular expression based on past messages
|
||||
package nuke
|
||||
|
||||
import (
|
||||
|
@ -32,6 +35,7 @@ var (
|
|||
ptrString10m = func(v string) *string { return &v }("10m")
|
||||
)
|
||||
|
||||
// Register provides the plugins.RegisterFunc
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
botTwitchClient = args.GetTwitchClient()
|
||||
formatMessage = args.FormatMessage
|
||||
|
@ -146,7 +150,7 @@ type (
|
|||
}
|
||||
)
|
||||
|
||||
func (a actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
rawMatch, err := formatMessage(attrs.MustString("match", nil), m, r, eventData)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "formatting match")
|
||||
|
@ -228,10 +232,10 @@ func (a actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData
|
|||
return false, nil
|
||||
}
|
||||
|
||||
func (a actor) IsAsync() bool { return false }
|
||||
func (a actor) Name() string { return actorName }
|
||||
func (actor) IsAsync() bool { return false }
|
||||
func (actor) Name() string { return actorName }
|
||||
|
||||
func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
if v, err := attrs.String("match"); err != nil || v == "" {
|
||||
return errors.New("match must be non-empty string")
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
// Package punish contains an actor to punish behaviour in a channel
|
||||
// with rising punishments
|
||||
package punish
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -29,6 +32,7 @@ var (
|
|||
ptrStringEmpty = func(v string) *string { return &v }("")
|
||||
)
|
||||
|
||||
// Register provides the plugins.RegisterFunc
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
db = args.GetDatabaseConnector()
|
||||
if err := db.DB().AutoMigrate(&punishLevel{}); err != nil {
|
||||
|
@ -36,7 +40,7 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
}
|
||||
|
||||
args.RegisterCopyDatabaseFunc("punish", func(src, target *gorm.DB) error {
|
||||
return database.CopyObjects(src, target, &punishLevel{})
|
||||
return database.CopyObjects(src, target, &punishLevel{}) //nolint:wrapcheck // internal helper
|
||||
})
|
||||
|
||||
botTwitchClient = args.GetTwitchClient()
|
||||
|
@ -142,7 +146,7 @@ type (
|
|||
|
||||
// Punish
|
||||
|
||||
func (a actorPunish) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
func (actorPunish) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
var (
|
||||
cooldown = attrs.MustDuration("cooldown", ptrDefaultCooldown)
|
||||
reason = attrs.MustString("reason", ptrStringEmpty)
|
||||
|
@ -168,6 +172,7 @@ func (a actorPunish) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eve
|
|||
switch lt := levels[nLvl]; lt {
|
||||
case "ban":
|
||||
if err = botTwitchClient.BanUser(
|
||||
context.Background(),
|
||||
plugins.DeriveChannel(m, eventData),
|
||||
strings.TrimLeft(user, "@"),
|
||||
0,
|
||||
|
@ -183,6 +188,7 @@ func (a actorPunish) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eve
|
|||
}
|
||||
|
||||
if err = botTwitchClient.DeleteMessage(
|
||||
context.Background(),
|
||||
plugins.DeriveChannel(m, eventData),
|
||||
msgID,
|
||||
); err != nil {
|
||||
|
@ -196,6 +202,7 @@ func (a actorPunish) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eve
|
|||
}
|
||||
|
||||
if err = botTwitchClient.BanUser(
|
||||
context.Background(),
|
||||
plugins.DeriveChannel(m, eventData),
|
||||
strings.TrimLeft(user, "@"),
|
||||
to,
|
||||
|
@ -215,10 +222,10 @@ func (a actorPunish) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eve
|
|||
)
|
||||
}
|
||||
|
||||
func (a actorPunish) IsAsync() bool { return false }
|
||||
func (a actorPunish) Name() string { return actorNamePunish }
|
||||
func (actorPunish) IsAsync() bool { return false }
|
||||
func (actorPunish) Name() string { return actorNamePunish }
|
||||
|
||||
func (a actorPunish) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
func (actorPunish) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
if v, err := attrs.String("user"); err != nil || v == "" {
|
||||
return errors.New("user must be non-empty string")
|
||||
}
|
||||
|
@ -236,7 +243,7 @@ func (a actorPunish) Validate(tplValidator plugins.TemplateValidatorFunc, attrs
|
|||
|
||||
// Reset
|
||||
|
||||
func (a actorResetPunish) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
func (actorResetPunish) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
var (
|
||||
user = attrs.MustString("user", nil)
|
||||
uuid = attrs.MustString("uuid", ptrStringEmpty)
|
||||
|
@ -252,10 +259,10 @@ func (a actorResetPunish) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule
|
|||
)
|
||||
}
|
||||
|
||||
func (a actorResetPunish) IsAsync() bool { return false }
|
||||
func (a actorResetPunish) Name() string { return actorNameResetPunish }
|
||||
func (actorResetPunish) IsAsync() bool { return false }
|
||||
func (actorResetPunish) Name() string { return actorNameResetPunish }
|
||||
|
||||
func (a actorResetPunish) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
func (actorResetPunish) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
if v, err := attrs.String("user"); err != nil || v == "" {
|
||||
return errors.New("user must be non-empty string")
|
||||
}
|
||||
|
|
|
@ -94,7 +94,7 @@ func getPunishment(db database.Connector, channel, user, uuid string) (*levelCon
|
|||
err := helpers.Retry(func() error {
|
||||
err := db.DB().First(&p, "key = ?", getDBKey(channel, user, uuid)).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return backoff.NewErrCannotRetry(err)
|
||||
return backoff.NewErrCannotRetry(err) //nolint:wrapcheck // we get our internal error
|
||||
}
|
||||
return err
|
||||
})
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
// Package quotedb contains a quote database and actor / api methods
|
||||
// to manage it
|
||||
package quotedb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
@ -25,14 +28,15 @@ var (
|
|||
ptrStringZero = func(v string) *string { return &v }("0")
|
||||
)
|
||||
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
// Register provides the plugins.RegisterFunc
|
||||
func Register(args plugins.RegistrationArguments) (err error) {
|
||||
db = args.GetDatabaseConnector()
|
||||
if err := db.DB().AutoMigrate("e{}); err != nil {
|
||||
if err = db.DB().AutoMigrate("e{}); err != nil {
|
||||
return errors.Wrap(err, "applying schema migration")
|
||||
}
|
||||
|
||||
args.RegisterCopyDatabaseFunc("quote", func(src, target *gorm.DB) error {
|
||||
return database.CopyObjects(src, target, "e{})
|
||||
return database.CopyObjects(src, target, "e{}) //nolint:wrapcheck // internal helper
|
||||
})
|
||||
|
||||
formatMessage = args.FormatMessage
|
||||
|
@ -85,11 +89,13 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
},
|
||||
})
|
||||
|
||||
registerAPI(args.RegisterAPIRoute)
|
||||
if err = registerAPI(args.RegisterAPIRoute); err != nil {
|
||||
return fmt.Errorf("registering API: %w", err)
|
||||
}
|
||||
|
||||
args.RegisterTemplateFunction("lastQuoteIndex", func(m *irc.Message, r *plugins.Rule, fields *plugins.FieldCollection) interface{} {
|
||||
return func() (int, error) {
|
||||
return GetMaxQuoteIdx(db, plugins.DeriveChannel(m, nil))
|
||||
return getMaxQuoteIdx(db, plugins.DeriveChannel(m, nil))
|
||||
}
|
||||
}, plugins.TemplateFuncDocumentation{
|
||||
Description: "Gets the last quote index in the quote database for the current channel",
|
||||
|
@ -107,7 +113,7 @@ type (
|
|||
actor struct{}
|
||||
)
|
||||
|
||||
func (a actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
var (
|
||||
action = attrs.MustString("action", ptrStringEmpty)
|
||||
indexStr = attrs.MustString("index", ptrStringZero)
|
||||
|
@ -135,18 +141,18 @@ func (a actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData
|
|||
}
|
||||
|
||||
return false, errors.Wrap(
|
||||
AddQuote(db, plugins.DeriveChannel(m, eventData), quote),
|
||||
addQuote(db, plugins.DeriveChannel(m, eventData), quote),
|
||||
"adding quote",
|
||||
)
|
||||
|
||||
case "del":
|
||||
return false, errors.Wrap(
|
||||
DelQuote(db, plugins.DeriveChannel(m, eventData), index),
|
||||
delQuote(db, plugins.DeriveChannel(m, eventData), index),
|
||||
"storing quote database",
|
||||
)
|
||||
|
||||
case "get":
|
||||
idx, quote, err := GetQuote(db, plugins.DeriveChannel(m, eventData), index)
|
||||
idx, quote, err := getQuote(db, plugins.DeriveChannel(m, eventData), index)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "getting quote")
|
||||
}
|
||||
|
@ -181,10 +187,10 @@ func (a actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData
|
|||
return false, nil
|
||||
}
|
||||
|
||||
func (a actor) IsAsync() bool { return false }
|
||||
func (a actor) Name() string { return actorName }
|
||||
func (actor) IsAsync() bool { return false }
|
||||
func (actor) Name() string { return actorName }
|
||||
|
||||
func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
action := attrs.MustString("action", ptrStringEmpty)
|
||||
|
||||
switch action {
|
||||
|
|
|
@ -20,7 +20,7 @@ type (
|
|||
}
|
||||
)
|
||||
|
||||
func AddQuote(db database.Connector, channel, quoteStr string) error {
|
||||
func addQuote(db database.Connector, channel, quoteStr string) error {
|
||||
return errors.Wrap(
|
||||
helpers.RetryTransaction(db.DB(), func(tx *gorm.DB) error {
|
||||
return tx.Create("e{
|
||||
|
@ -33,8 +33,8 @@ func AddQuote(db database.Connector, channel, quoteStr string) error {
|
|||
)
|
||||
}
|
||||
|
||||
func DelQuote(db database.Connector, channel string, quoteIdx int) error {
|
||||
_, createdAt, _, err := GetQuoteRaw(db, channel, quoteIdx)
|
||||
func delQuote(db database.Connector, channel string, quoteIdx int) error {
|
||||
_, createdAt, _, err := getQuoteRaw(db, channel, quoteIdx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "fetching specified quote")
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ func DelQuote(db database.Connector, channel string, quoteIdx int) error {
|
|||
)
|
||||
}
|
||||
|
||||
func GetChannelQuotes(db database.Connector, channel string) ([]string, error) {
|
||||
func getChannelQuotes(db database.Connector, channel string) ([]string, error) {
|
||||
var qs []quote
|
||||
if err := helpers.Retry(func() error {
|
||||
return db.DB().Where("channel = ?", channel).Order("created_at").Find(&qs).Error
|
||||
|
@ -63,7 +63,7 @@ func GetChannelQuotes(db database.Connector, channel string) ([]string, error) {
|
|||
return quotes, nil
|
||||
}
|
||||
|
||||
func GetMaxQuoteIdx(db database.Connector, channel string) (int, error) {
|
||||
func getMaxQuoteIdx(db database.Connector, channel string) (int, error) {
|
||||
var count int64
|
||||
if err := helpers.Retry(func() error {
|
||||
return db.DB().
|
||||
|
@ -78,14 +78,14 @@ func GetMaxQuoteIdx(db database.Connector, channel string) (int, error) {
|
|||
return int(count), nil
|
||||
}
|
||||
|
||||
func GetQuote(db database.Connector, channel string, quote int) (int, string, error) {
|
||||
quoteIdx, _, quoteText, err := GetQuoteRaw(db, channel, quote)
|
||||
func getQuote(db database.Connector, channel string, quote int) (int, string, error) {
|
||||
quoteIdx, _, quoteText, err := getQuoteRaw(db, channel, quote)
|
||||
return quoteIdx, quoteText, err
|
||||
}
|
||||
|
||||
func GetQuoteRaw(db database.Connector, channel string, quoteIdx int) (int, int64, string, error) {
|
||||
func getQuoteRaw(db database.Connector, channel string, quoteIdx int) (int, int64, string, error) {
|
||||
if quoteIdx == 0 {
|
||||
max, err := GetMaxQuoteIdx(db, channel)
|
||||
max, err := getMaxQuoteIdx(db, channel)
|
||||
if err != nil {
|
||||
return 0, 0, "", errors.Wrap(err, "getting max quote idx")
|
||||
}
|
||||
|
@ -113,7 +113,7 @@ func GetQuoteRaw(db database.Connector, channel string, quoteIdx int) (int, int6
|
|||
}
|
||||
}
|
||||
|
||||
func SetQuotes(db database.Connector, channel string, quotes []string) error {
|
||||
func setQuotes(db database.Connector, channel string, quotes []string) error {
|
||||
return errors.Wrap(
|
||||
helpers.RetryTransaction(db.DB(), func(tx *gorm.DB) error {
|
||||
if err := tx.Where("channel = ?", channel).Delete("e{}).Error; err != nil {
|
||||
|
@ -139,8 +139,8 @@ func SetQuotes(db database.Connector, channel string, quotes []string) error {
|
|||
)
|
||||
}
|
||||
|
||||
func UpdateQuote(db database.Connector, channel string, idx int, quoteStr string) error {
|
||||
_, createdAt, _, err := GetQuoteRaw(db, channel, idx)
|
||||
func updateQuote(db database.Connector, channel string, idx int, quoteStr string) error {
|
||||
_, createdAt, _, err := getQuoteRaw(db, channel, idx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "fetching specified quote")
|
||||
}
|
||||
|
|
|
@ -24,37 +24,37 @@ func TestQuotesRoundtrip(t *testing.T) {
|
|||
}
|
||||
)
|
||||
|
||||
cq, err := GetChannelQuotes(dbc, channel)
|
||||
cq, err := getChannelQuotes(dbc, channel)
|
||||
assert.NoError(t, err, "querying empty database")
|
||||
assert.Zero(t, cq, "expecting no quotes")
|
||||
|
||||
for i, q := range quotes {
|
||||
assert.NoError(t, AddQuote(dbc, channel, q), "adding quote %d", i)
|
||||
assert.NoError(t, addQuote(dbc, channel, q), "adding quote %d", i)
|
||||
}
|
||||
|
||||
cq, err = GetChannelQuotes(dbc, channel)
|
||||
cq, err = getChannelQuotes(dbc, channel)
|
||||
assert.NoError(t, err, "querying database")
|
||||
assert.Equal(t, quotes, cq, "checkin order and presence of quotes")
|
||||
|
||||
assert.NoError(t, DelQuote(dbc, channel, 1), "removing one quote")
|
||||
assert.NoError(t, DelQuote(dbc, channel, 1), "removing one quote")
|
||||
assert.NoError(t, delQuote(dbc, channel, 1), "removing one quote")
|
||||
assert.NoError(t, delQuote(dbc, channel, 1), "removing one quote")
|
||||
|
||||
cq, err = GetChannelQuotes(dbc, channel)
|
||||
cq, err = getChannelQuotes(dbc, channel)
|
||||
assert.NoError(t, err, "querying database")
|
||||
assert.Len(t, cq, len(quotes)-2, "expecting quotes in db")
|
||||
|
||||
assert.NoError(t, SetQuotes(dbc, channel, quotes), "replacing quotes")
|
||||
assert.NoError(t, setQuotes(dbc, channel, quotes), "replacing quotes")
|
||||
|
||||
cq, err = GetChannelQuotes(dbc, channel)
|
||||
cq, err = getChannelQuotes(dbc, channel)
|
||||
assert.NoError(t, err, "querying database")
|
||||
assert.Equal(t, quotes, cq, "checkin order and presence of quotes")
|
||||
|
||||
idx, q, err := GetQuote(dbc, channel, 0)
|
||||
idx, q, err := getQuote(dbc, channel, 0)
|
||||
assert.NoError(t, err, "getting random quote")
|
||||
assert.NotZero(t, idx, "index must not be zero")
|
||||
assert.NotZero(t, q, "quote must not be zero")
|
||||
|
||||
idx, q, err = GetQuote(dbc, channel, 3)
|
||||
idx, q, err = getQuote(dbc, channel, 3)
|
||||
assert.NoError(t, err, "getting specific quote")
|
||||
assert.Equal(t, 3, idx, "index must be 3")
|
||||
assert.Equal(t, quotes[2], q, "quote must not the third")
|
||||
|
|
|
@ -3,6 +3,7 @@ package quotedb
|
|||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -20,16 +21,19 @@ var (
|
|||
listScript []byte
|
||||
)
|
||||
|
||||
func registerAPI(register plugins.HTTPRouteRegistrationFunc) {
|
||||
register(plugins.HTTPRouteRegistrationArgs{
|
||||
//nolint:funlen
|
||||
func registerAPI(register plugins.HTTPRouteRegistrationFunc) (err error) {
|
||||
if err = register(plugins.HTTPRouteRegistrationArgs{
|
||||
HandlerFunc: handleScript,
|
||||
Method: http.MethodGet,
|
||||
Module: "quotedb",
|
||||
Path: "/app.js",
|
||||
SkipDocumentation: true,
|
||||
})
|
||||
}); err != nil {
|
||||
return fmt.Errorf("registering API route: %w", err)
|
||||
}
|
||||
|
||||
register(plugins.HTTPRouteRegistrationArgs{
|
||||
if err = register(plugins.HTTPRouteRegistrationArgs{
|
||||
Description: "Add quotes for the given {channel}",
|
||||
HandlerFunc: handleAddQuotes,
|
||||
Method: http.MethodPost,
|
||||
|
@ -44,9 +48,11 @@ func registerAPI(register plugins.HTTPRouteRegistrationFunc) {
|
|||
Name: "channel",
|
||||
},
|
||||
},
|
||||
})
|
||||
}); err != nil {
|
||||
return fmt.Errorf("registering API route: %w", err)
|
||||
}
|
||||
|
||||
register(plugins.HTTPRouteRegistrationArgs{
|
||||
if err = register(plugins.HTTPRouteRegistrationArgs{
|
||||
Description: "Deletes quote with given {idx} from {channel}",
|
||||
HandlerFunc: handleDeleteQuote,
|
||||
Method: http.MethodDelete,
|
||||
|
@ -65,9 +71,11 @@ func registerAPI(register plugins.HTTPRouteRegistrationFunc) {
|
|||
Name: "idx",
|
||||
},
|
||||
},
|
||||
})
|
||||
}); err != nil {
|
||||
return fmt.Errorf("registering API route: %w", err)
|
||||
}
|
||||
|
||||
register(plugins.HTTPRouteRegistrationArgs{
|
||||
if err = register(plugins.HTTPRouteRegistrationArgs{
|
||||
Accept: []string{"application/json", "text/html"},
|
||||
Description: "Lists all quotes for the given {channel}",
|
||||
HandlerFunc: handleListQuotes,
|
||||
|
@ -82,9 +90,11 @@ func registerAPI(register plugins.HTTPRouteRegistrationFunc) {
|
|||
Name: "channel",
|
||||
},
|
||||
},
|
||||
})
|
||||
}); err != nil {
|
||||
return fmt.Errorf("registering API route: %w", err)
|
||||
}
|
||||
|
||||
register(plugins.HTTPRouteRegistrationArgs{
|
||||
if err = register(plugins.HTTPRouteRegistrationArgs{
|
||||
Description: "Set quotes for the given {channel} (will overwrite ALL quotes!)",
|
||||
HandlerFunc: handleReplaceQuotes,
|
||||
Method: http.MethodPut,
|
||||
|
@ -99,9 +109,11 @@ func registerAPI(register plugins.HTTPRouteRegistrationFunc) {
|
|||
Name: "channel",
|
||||
},
|
||||
},
|
||||
})
|
||||
}); err != nil {
|
||||
return fmt.Errorf("registering API route: %w", err)
|
||||
}
|
||||
|
||||
register(plugins.HTTPRouteRegistrationArgs{
|
||||
if err = register(plugins.HTTPRouteRegistrationArgs{
|
||||
Description: "Updates quote with given {idx} from {channel}",
|
||||
HandlerFunc: handleUpdateQuote,
|
||||
Method: http.MethodPut,
|
||||
|
@ -120,7 +132,11 @@ func registerAPI(register plugins.HTTPRouteRegistrationFunc) {
|
|||
Name: "idx",
|
||||
},
|
||||
},
|
||||
})
|
||||
}); err != nil {
|
||||
return fmt.Errorf("registering API route: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleAddQuotes(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -133,7 +149,7 @@ func handleAddQuotes(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
for _, q := range quotes {
|
||||
if err := AddQuote(db, channel, q); err != nil {
|
||||
if err := addQuote(db, channel, q); err != nil {
|
||||
http.Error(w, errors.Wrap(err, "adding quote").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
@ -154,7 +170,7 @@ func handleDeleteQuote(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if err = DelQuote(db, channel, idx); err != nil {
|
||||
if err = delQuote(db, channel, idx); err != nil {
|
||||
http.Error(w, errors.Wrap(err, "deleting quote").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
@ -165,13 +181,13 @@ func handleDeleteQuote(w http.ResponseWriter, r *http.Request) {
|
|||
func handleListQuotes(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasPrefix(r.Header.Get("Accept"), "text/html") {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write(listFrontend)
|
||||
w.Write(listFrontend) //nolint:errcheck,gosec,revive
|
||||
return
|
||||
}
|
||||
|
||||
channel := "#" + strings.TrimLeft(mux.Vars(r)["channel"], "#")
|
||||
|
||||
quotes, err := GetChannelQuotes(db, channel)
|
||||
quotes, err := getChannelQuotes(db, channel)
|
||||
if err != nil {
|
||||
http.Error(w, errors.Wrap(err, "getting quotes").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
@ -192,7 +208,7 @@ func handleReplaceQuotes(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if err := SetQuotes(db, channel, quotes); err != nil {
|
||||
if err := setQuotes(db, channel, quotes); err != nil {
|
||||
http.Error(w, errors.Wrap(err, "replacing quotes").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
@ -202,7 +218,7 @@ func handleReplaceQuotes(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
func handleScript(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/javascript")
|
||||
w.Write(listScript)
|
||||
w.Write(listScript) //nolint:errcheck,gosec,revive
|
||||
}
|
||||
|
||||
func handleUpdateQuote(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -228,7 +244,7 @@ func handleUpdateQuote(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if err = UpdateQuote(db, channel, idx, quotes[0]); err != nil {
|
||||
if err = updateQuote(db, channel, idx, quotes[0]); err != nil {
|
||||
http.Error(w, errors.Wrap(err, "updating quote").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// Package raw contains an actor to send raw IRC messages
|
||||
package raw
|
||||
|
||||
import (
|
||||
|
@ -16,6 +17,7 @@ var (
|
|||
ptrStringEmpty = func(s string) *string { return &s }("")
|
||||
)
|
||||
|
||||
// Register provides the plugins.RegisterFunc
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
formatMessage = args.FormatMessage
|
||||
send = args.SendMessage
|
||||
|
@ -45,7 +47,7 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
|
||||
type actor struct{}
|
||||
|
||||
func (a actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
rawMsg, err := formatMessage(attrs.MustString("message", nil), m, r, eventData)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "preparing raw message")
|
||||
|
@ -62,10 +64,10 @@ func (a actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData
|
|||
)
|
||||
}
|
||||
|
||||
func (a actor) IsAsync() bool { return false }
|
||||
func (a actor) Name() string { return actorName }
|
||||
func (actor) IsAsync() bool { return false }
|
||||
func (actor) Name() string { return actorName }
|
||||
|
||||
func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
if v, err := attrs.String("message"); err != nil || v == "" {
|
||||
return errors.New("message must be non-empty string")
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// Package respond contains an actor to send a message
|
||||
package respond
|
||||
|
||||
import (
|
||||
|
@ -24,7 +25,8 @@ var (
|
|||
ptrStringEmpty = func(s string) *string { return &s }("")
|
||||
)
|
||||
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
// Register provides the plugins.RegisterFunc
|
||||
func Register(args plugins.RegistrationArguments) (err error) {
|
||||
formatMessage = args.FormatMessage
|
||||
send = args.SendMessage
|
||||
|
||||
|
@ -76,7 +78,7 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
},
|
||||
})
|
||||
|
||||
args.RegisterAPIRoute(plugins.HTTPRouteRegistrationArgs{
|
||||
if err = args.RegisterAPIRoute(plugins.HTTPRouteRegistrationArgs{
|
||||
Description: "Send a message on behalf of the bot (send JSON object with `message` key)",
|
||||
HandlerFunc: handleAPISend,
|
||||
Method: http.MethodPost,
|
||||
|
@ -91,14 +93,16 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
Name: "channel",
|
||||
},
|
||||
},
|
||||
})
|
||||
}); err != nil {
|
||||
return fmt.Errorf("registering API route: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type actor struct{}
|
||||
|
||||
func (a actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
msg, err := formatMessage(attrs.MustString("message", nil), m, r, eventData)
|
||||
if err != nil {
|
||||
if !attrs.CanString("fallback") || attrs.MustString("fallback", nil) == "" {
|
||||
|
@ -139,10 +143,10 @@ func (a actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData
|
|||
)
|
||||
}
|
||||
|
||||
func (a actor) IsAsync() bool { return false }
|
||||
func (a actor) Name() string { return actorName }
|
||||
func (actor) IsAsync() bool { return false }
|
||||
func (actor) Name() string { return actorName }
|
||||
|
||||
func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
if v, err := attrs.String("message"); err != nil || v == "" {
|
||||
return errors.New("message must be non-empty string")
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// Package shield contains an actor to update the shield-mode for a
|
||||
// given channel
|
||||
package shield
|
||||
|
||||
import (
|
||||
|
@ -14,6 +16,7 @@ const actorName = "shield"
|
|||
|
||||
var botTwitchClient *twitch.Client
|
||||
|
||||
// Register provides the plugins.RegisterFunc
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
botTwitchClient = args.GetTwitchClient()
|
||||
|
||||
|
@ -42,7 +45,7 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
|
||||
type actor struct{}
|
||||
|
||||
func (a actor) Execute(_ *irc.Client, m *irc.Message, _ *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
func (actor) Execute(_ *irc.Client, m *irc.Message, _ *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
ptrBoolFalse := func(v bool) *bool { return &v }(false)
|
||||
|
||||
return false, errors.Wrap(
|
||||
|
@ -55,10 +58,10 @@ func (a actor) Execute(_ *irc.Client, m *irc.Message, _ *plugins.Rule, eventData
|
|||
)
|
||||
}
|
||||
|
||||
func (a actor) IsAsync() bool { return false }
|
||||
func (a actor) Name() string { return actorName }
|
||||
func (actor) IsAsync() bool { return false }
|
||||
func (actor) Name() string { return actorName }
|
||||
|
||||
func (a actor) Validate(_ plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
func (actor) Validate(_ plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
if _, err = attrs.Bool("enable"); err != nil {
|
||||
return errors.New("enable must be boolean")
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
// Package shoutout contains an actor to create a Twitch native
|
||||
// shoutout
|
||||
package shoutout
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
@ -20,6 +23,7 @@ var (
|
|||
shoutoutChatcommandRegex = regexp.MustCompile(`^/shoutout +([^\s]+)$`)
|
||||
)
|
||||
|
||||
// Register provides the plugins.RegisterFunc
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
botTwitchClient = args.GetTwitchClient()
|
||||
formatMessage = args.FormatMessage
|
||||
|
@ -51,7 +55,7 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
|
||||
type actor struct{}
|
||||
|
||||
func (a actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
user, err := formatMessage(attrs.MustString("user", ptrStringEmpty), m, r, eventData)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "executing user template")
|
||||
|
@ -59,6 +63,7 @@ func (a actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData
|
|||
|
||||
return false, errors.Wrap(
|
||||
botTwitchClient.SendShoutout(
|
||||
context.Background(),
|
||||
plugins.DeriveChannel(m, eventData),
|
||||
user,
|
||||
),
|
||||
|
@ -66,10 +71,10 @@ func (a actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData
|
|||
)
|
||||
}
|
||||
|
||||
func (a actor) IsAsync() bool { return false }
|
||||
func (a actor) Name() string { return actorName }
|
||||
func (actor) IsAsync() bool { return false }
|
||||
func (actor) Name() string { return actorName }
|
||||
|
||||
func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
if v, err := attrs.String("user"); err != nil || v == "" {
|
||||
return errors.New("user must be non-empty string")
|
||||
}
|
||||
|
@ -89,7 +94,7 @@ func handleChatCommand(m *irc.Message) error {
|
|||
return errors.New("shoutout message does not match required format")
|
||||
}
|
||||
|
||||
if err := botTwitchClient.SendShoutout(channel, matches[1]); err != nil {
|
||||
if err := botTwitchClient.SendShoutout(context.Background(), channel, matches[1]); err != nil {
|
||||
return errors.Wrap(err, "executing shoutout")
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// Package stopexec contains an actor to stop the rule execution on
|
||||
// template condition
|
||||
package stopexec
|
||||
|
||||
import (
|
||||
|
@ -11,6 +13,7 @@ const actorName = "stopexec"
|
|||
|
||||
var formatMessage plugins.MsgFormatter
|
||||
|
||||
// Register provides the plugins.RegisterFunc
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
formatMessage = args.FormatMessage
|
||||
|
||||
|
@ -39,7 +42,7 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
|
||||
type actor struct{}
|
||||
|
||||
func (a actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
ptrStringEmpty := func(v string) *string { return &v }("")
|
||||
|
||||
when, err := formatMessage(attrs.MustString("when", ptrStringEmpty), m, r, eventData)
|
||||
|
@ -54,10 +57,10 @@ func (a actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData
|
|||
return false, nil
|
||||
}
|
||||
|
||||
func (a actor) IsAsync() bool { return false }
|
||||
func (a actor) Name() string { return actorName }
|
||||
func (actor) IsAsync() bool { return false }
|
||||
func (actor) Name() string { return actorName }
|
||||
|
||||
func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
whenTemplate, err := attrs.String("when")
|
||||
if err != nil || whenTemplate == "" {
|
||||
return errors.New("when must be non-empty string")
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
// Package timeout contains an actor to timeout users
|
||||
package timeout
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
@ -22,6 +24,7 @@ var (
|
|||
timeoutChatcommandRegex = regexp.MustCompile(`^/timeout +([^\s]+) +([0-9]+) +(.+)$`)
|
||||
)
|
||||
|
||||
// Register provides the plugins.RegisterFunc
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
botTwitchClient = args.GetTwitchClient()
|
||||
formatMessage = args.FormatMessage
|
||||
|
@ -62,7 +65,7 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
|
||||
type actor struct{}
|
||||
|
||||
func (a actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
reason, err := formatMessage(attrs.MustString("reason", ptrStringEmpty), m, r, eventData)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "executing reason template")
|
||||
|
@ -70,6 +73,7 @@ func (a actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData
|
|||
|
||||
return false, errors.Wrap(
|
||||
botTwitchClient.BanUser(
|
||||
context.Background(),
|
||||
plugins.DeriveChannel(m, eventData),
|
||||
plugins.DeriveUser(m, eventData),
|
||||
attrs.MustDuration("duration", nil),
|
||||
|
@ -79,10 +83,10 @@ func (a actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData
|
|||
)
|
||||
}
|
||||
|
||||
func (a actor) IsAsync() bool { return false }
|
||||
func (a actor) Name() string { return actorName }
|
||||
func (actor) IsAsync() bool { return false }
|
||||
func (actor) Name() string { return actorName }
|
||||
|
||||
func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
if v, err := attrs.Duration("duration"); err != nil || v < time.Second {
|
||||
return errors.New("duration must be of type duration greater or equal one second")
|
||||
}
|
||||
|
@ -111,7 +115,7 @@ func handleChatCommand(m *irc.Message) error {
|
|||
return errors.Wrap(err, "parsing timeout duration")
|
||||
}
|
||||
|
||||
if err = botTwitchClient.BanUser(channel, matches[1], time.Duration(duration)*time.Second, matches[3]); err != nil {
|
||||
if err = botTwitchClient.BanUser(context.Background(), channel, matches[1], time.Duration(duration)*time.Second, matches[3]); err != nil {
|
||||
return errors.Wrap(err, "executing timeout")
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// Package variables contains an actor and database client to store
|
||||
// handle variables
|
||||
package variables
|
||||
|
||||
import (
|
||||
|
@ -21,20 +23,22 @@ var (
|
|||
ptrStringEmpty = func(s string) *string { return &s }("")
|
||||
)
|
||||
|
||||
// Register provides the plugins.RegisterFunc
|
||||
//
|
||||
//nolint:funlen // Function contains only documentation registration
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
func Register(args plugins.RegistrationArguments) (err error) {
|
||||
db = args.GetDatabaseConnector()
|
||||
if err := db.DB().AutoMigrate(&variable{}); err != nil {
|
||||
if err = db.DB().AutoMigrate(&variable{}); err != nil {
|
||||
return errors.Wrap(err, "applying schema migration")
|
||||
}
|
||||
|
||||
args.RegisterCopyDatabaseFunc("variable", func(src, target *gorm.DB) error {
|
||||
return database.CopyObjects(src, target, &variable{})
|
||||
return database.CopyObjects(src, target, &variable{}) //nolint:wrapcheck // internal helper
|
||||
})
|
||||
|
||||
formatMessage = args.FormatMessage
|
||||
|
||||
args.RegisterActor("setvariable", func() plugins.Actor { return &ActorSetVariable{} })
|
||||
args.RegisterActor("setvariable", func() plugins.Actor { return &actorSetVariable{} })
|
||||
|
||||
args.RegisterActorDocumentation(plugins.ActionDocumentation{
|
||||
Description: "Modify variable contents",
|
||||
|
@ -72,7 +76,7 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
},
|
||||
})
|
||||
|
||||
args.RegisterAPIRoute(plugins.HTTPRouteRegistrationArgs{
|
||||
if err = args.RegisterAPIRoute(plugins.HTTPRouteRegistrationArgs{
|
||||
Description: "Returns the value as a plain string",
|
||||
HandlerFunc: routeActorSetVarGetValue,
|
||||
Method: http.MethodGet,
|
||||
|
@ -86,9 +90,11 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
Name: "name",
|
||||
},
|
||||
},
|
||||
})
|
||||
}); err != nil {
|
||||
return fmt.Errorf("registering API route: %w", err)
|
||||
}
|
||||
|
||||
args.RegisterAPIRoute(plugins.HTTPRouteRegistrationArgs{
|
||||
if err = args.RegisterAPIRoute(plugins.HTTPRouteRegistrationArgs{
|
||||
Description: "Updates the value of the variable",
|
||||
HandlerFunc: routeActorSetVarSetValue,
|
||||
Method: http.MethodPatch,
|
||||
|
@ -110,10 +116,12 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
Name: "name",
|
||||
},
|
||||
},
|
||||
})
|
||||
}); err != nil {
|
||||
return fmt.Errorf("registering API route: %w", err)
|
||||
}
|
||||
|
||||
args.RegisterTemplateFunction("variable", plugins.GenericTemplateFunctionGetter(func(name string, defVal ...string) (string, error) {
|
||||
value, err := GetVariable(db, name)
|
||||
value, err := getVariable(db, name)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "getting variable")
|
||||
}
|
||||
|
@ -134,9 +142,9 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
type ActorSetVariable struct{}
|
||||
type actorSetVariable struct{}
|
||||
|
||||
func (a ActorSetVariable) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
func (actorSetVariable) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
varName, err := formatMessage(attrs.MustString("variable", nil), m, r, eventData)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "preparing variable name")
|
||||
|
@ -144,7 +152,7 @@ func (a ActorSetVariable) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule
|
|||
|
||||
if attrs.MustBool("clear", ptrBoolFalse) {
|
||||
return false, errors.Wrap(
|
||||
RemoveVariable(db, varName),
|
||||
removeVariable(db, varName),
|
||||
"removing variable",
|
||||
)
|
||||
}
|
||||
|
@ -155,15 +163,15 @@ func (a ActorSetVariable) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule
|
|||
}
|
||||
|
||||
return false, errors.Wrap(
|
||||
SetVariable(db, varName, value),
|
||||
setVariable(db, varName, value),
|
||||
"setting variable",
|
||||
)
|
||||
}
|
||||
|
||||
func (a ActorSetVariable) IsAsync() bool { return false }
|
||||
func (a ActorSetVariable) Name() string { return "setvariable" }
|
||||
func (actorSetVariable) IsAsync() bool { return false }
|
||||
func (actorSetVariable) Name() string { return "setvariable" }
|
||||
|
||||
func (a ActorSetVariable) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
func (actorSetVariable) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
if v, err := attrs.String("variable"); err != nil || v == "" {
|
||||
return errors.New("variable name must be non-empty string")
|
||||
}
|
||||
|
@ -178,7 +186,7 @@ func (a ActorSetVariable) Validate(tplValidator plugins.TemplateValidatorFunc, a
|
|||
}
|
||||
|
||||
func routeActorSetVarGetValue(w http.ResponseWriter, r *http.Request) {
|
||||
vc, err := GetVariable(db, mux.Vars(r)["name"])
|
||||
vc, err := getVariable(db, mux.Vars(r)["name"])
|
||||
if err != nil {
|
||||
http.Error(w, errors.Wrap(err, "getting value").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
@ -189,7 +197,7 @@ func routeActorSetVarGetValue(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
func routeActorSetVarSetValue(w http.ResponseWriter, r *http.Request) {
|
||||
if err := SetVariable(db, mux.Vars(r)["name"], r.FormValue("value")); err != nil {
|
||||
if err := setVariable(db, mux.Vars(r)["name"], r.FormValue("value")); err != nil {
|
||||
http.Error(w, errors.Wrap(err, "updating value").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -17,12 +17,12 @@ type (
|
|||
}
|
||||
)
|
||||
|
||||
func GetVariable(db database.Connector, key string) (string, error) {
|
||||
func getVariable(db database.Connector, key string) (string, error) {
|
||||
var v variable
|
||||
err := helpers.Retry(func() error {
|
||||
err := db.DB().First(&v, "name = ?", key).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return backoff.NewErrCannotRetry(err)
|
||||
return backoff.NewErrCannotRetry(err) //nolint:wrapcheck // we get our internal error
|
||||
}
|
||||
return err
|
||||
})
|
||||
|
@ -38,7 +38,7 @@ func GetVariable(db database.Connector, key string) (string, error) {
|
|||
}
|
||||
}
|
||||
|
||||
func SetVariable(db database.Connector, key, value string) error {
|
||||
func setVariable(db database.Connector, key, value string) error {
|
||||
return errors.Wrap(
|
||||
helpers.RetryTransaction(db.DB(), func(tx *gorm.DB) error {
|
||||
return tx.Clauses(clause.OnConflict{
|
||||
|
@ -50,7 +50,7 @@ func SetVariable(db database.Connector, key, value string) error {
|
|||
)
|
||||
}
|
||||
|
||||
func RemoveVariable(db database.Connector, key string) error {
|
||||
func removeVariable(db database.Connector, key string) error {
|
||||
return errors.Wrap(
|
||||
helpers.RetryTransaction(db.DB(), func(tx *gorm.DB) error {
|
||||
return tx.Delete(&variable{}, "name = ?", key).Error
|
||||
|
|
|
@ -18,19 +18,19 @@ func TestVariableRoundtrip(t *testing.T) {
|
|||
testValue = "ee5e4be5-f292-48aa-a177-cb9fd6f4e171"
|
||||
)
|
||||
|
||||
v, err := GetVariable(dbc, name)
|
||||
v, err := getVariable(dbc, name)
|
||||
assert.NoError(t, err, "getting unset variable")
|
||||
assert.Zero(t, v, "checking zero state on unset variable")
|
||||
|
||||
assert.NoError(t, SetVariable(dbc, name, testValue), "setting variable")
|
||||
assert.NoError(t, setVariable(dbc, name, testValue), "setting variable")
|
||||
|
||||
v, err = GetVariable(dbc, name)
|
||||
v, err = getVariable(dbc, name)
|
||||
assert.NoError(t, err, "getting set variable")
|
||||
assert.NotZero(t, v, "checking non-zero state on set variable")
|
||||
|
||||
assert.NoError(t, RemoveVariable(dbc, name), "removing variable")
|
||||
assert.NoError(t, removeVariable(dbc, name), "removing variable")
|
||||
|
||||
v, err = GetVariable(dbc, name)
|
||||
v, err = getVariable(dbc, name)
|
||||
assert.NoError(t, err, "getting removed variable")
|
||||
assert.Zero(t, v, "checking zero state on removed variable")
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// Package vip contains actors to modify VIPs of a channel
|
||||
package vip
|
||||
|
||||
import (
|
||||
|
@ -19,6 +20,7 @@ var (
|
|||
ptrStringEmpty = func(s string) *string { return &s }("")
|
||||
)
|
||||
|
||||
// Register provides the plugins.RegisterFunc
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
formatMessage = args.FormatMessage
|
||||
permCheckFn = args.HasPermissionForChannel
|
||||
|
@ -96,7 +98,7 @@ type (
|
|||
)
|
||||
|
||||
func (actor) IsAsync() bool { return false }
|
||||
func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
for _, field := range []string{"channel", "user"} {
|
||||
if v, err := attrs.String(field); err != nil || v == "" {
|
||||
return errors.Errorf("%s must be non-empty string", field)
|
||||
|
@ -110,7 +112,7 @@ func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugi
|
|||
return nil
|
||||
}
|
||||
|
||||
func (a actor) getParams(m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (channel, user string, err error) {
|
||||
func (actor) getParams(m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (channel, user string, err error) {
|
||||
if channel, err = formatMessage(attrs.MustString("channel", nil), m, r, eventData); err != nil {
|
||||
return "", "", errors.Wrap(err, "parsing channel")
|
||||
}
|
||||
|
@ -129,7 +131,9 @@ func (u unvipActor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, even
|
|||
}
|
||||
|
||||
return false, errors.Wrap(
|
||||
executeModVIP(channel, func(tc *twitch.Client) error { return tc.RemoveChannelVIP(context.Background(), channel, user) }),
|
||||
executeModVIP(channel, func(tc *twitch.Client) error {
|
||||
return errors.Wrap(tc.RemoveChannelVIP(context.Background(), channel, user), "removing VIP")
|
||||
}),
|
||||
"removing VIP",
|
||||
)
|
||||
}
|
||||
|
@ -143,7 +147,9 @@ func (v vipActor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventD
|
|||
}
|
||||
|
||||
return false, errors.Wrap(
|
||||
executeModVIP(channel, func(tc *twitch.Client) error { return tc.AddChannelVIP(context.Background(), channel, user) }),
|
||||
executeModVIP(channel, func(tc *twitch.Client) error {
|
||||
return errors.Wrap(tc.AddChannelVIP(context.Background(), channel, user), "adding VIP")
|
||||
}),
|
||||
"adding VIP",
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
// Package whisper contains an actor to send whispers
|
||||
package whisper
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/irc.v4"
|
||||
|
||||
|
@ -17,6 +20,7 @@ var (
|
|||
ptrStringEmpty = func(s string) *string { return &s }("")
|
||||
)
|
||||
|
||||
// Register provides the plugins.RegisterFunc
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
botTwitchClient = args.GetTwitchClient()
|
||||
formatMessage = args.FormatMessage
|
||||
|
@ -55,7 +59,7 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
|
||||
type actor struct{}
|
||||
|
||||
func (a actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
to, err := formatMessage(attrs.MustString("to", nil), m, r, eventData)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "preparing whisper receiver")
|
||||
|
@ -67,15 +71,15 @@ func (a actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData
|
|||
}
|
||||
|
||||
return false, errors.Wrap(
|
||||
botTwitchClient.SendWhisper(to, msg),
|
||||
botTwitchClient.SendWhisper(context.Background(), to, msg),
|
||||
"sending whisper",
|
||||
)
|
||||
}
|
||||
|
||||
func (a actor) IsAsync() bool { return false }
|
||||
func (a actor) Name() string { return actorName }
|
||||
func (actor) IsAsync() bool { return false }
|
||||
func (actor) Name() string { return actorName }
|
||||
|
||||
func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
if v, err := attrs.String("to"); err != nil || v == "" {
|
||||
return errors.New("to must be non-empty string")
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import (
|
|||
|
||||
type actor struct{}
|
||||
|
||||
func (a actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
fd, err := formatMessage(attrs.MustString("fields", ptrStringEmpty), m, r, eventData)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "executing fields template")
|
||||
|
@ -32,10 +32,10 @@ func (a actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData
|
|||
)
|
||||
}
|
||||
|
||||
func (a actor) IsAsync() bool { return false }
|
||||
func (a actor) Name() string { return actorName }
|
||||
func (actor) IsAsync() bool { return false }
|
||||
func (actor) Name() string { return actorName }
|
||||
|
||||
func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
if v, err := attrs.String("fields"); err != nil || v == "" {
|
||||
return errors.New("fields is expected to be non-empty string")
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// Package customevent contains an actor and database modules to create
|
||||
// custom (timed) events
|
||||
package customevent
|
||||
|
||||
import (
|
||||
|
@ -27,14 +29,15 @@ var (
|
|||
ptrStringEmpty = func(s string) *string { return &s }("")
|
||||
)
|
||||
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
// Register provides the plugins.RegisterFunc
|
||||
func Register(args plugins.RegistrationArguments) (err error) {
|
||||
db = args.GetDatabaseConnector()
|
||||
if err := db.DB().AutoMigrate(&storedCustomEvent{}); err != nil {
|
||||
if err = db.DB().AutoMigrate(&storedCustomEvent{}); err != nil {
|
||||
return errors.Wrap(err, "applying schema migration")
|
||||
}
|
||||
|
||||
args.RegisterCopyDatabaseFunc("custom_event", func(src, target *gorm.DB) error {
|
||||
return database.CopyObjects(src, target, &storedCustomEvent{})
|
||||
return database.CopyObjects(src, target, &storedCustomEvent{}) //nolint:wrapcheck // internal helper
|
||||
})
|
||||
|
||||
mc = &memoryCache{dbc: db}
|
||||
|
@ -71,7 +74,7 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
},
|
||||
})
|
||||
|
||||
args.RegisterAPIRoute(plugins.HTTPRouteRegistrationArgs{
|
||||
if err = args.RegisterAPIRoute(plugins.HTTPRouteRegistrationArgs{
|
||||
Description: "Creates an `custom` event containing the fields provided in the request body",
|
||||
HandlerFunc: handleCreateEvent,
|
||||
Method: http.MethodPost,
|
||||
|
@ -94,7 +97,9 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
Name: "channel",
|
||||
},
|
||||
},
|
||||
})
|
||||
}); err != nil {
|
||||
return fmt.Errorf("registering API route: %w", err)
|
||||
}
|
||||
|
||||
for schedule, fn := range map[string]func(){
|
||||
fmt.Sprintf("@every %s", cleanupTimeout): scheduleCleanup,
|
||||
|
|
|
@ -57,6 +57,7 @@ func (m *memoryCache) Refresh() (err error) {
|
|||
return m.refresh()
|
||||
}
|
||||
|
||||
//revive:disable-next-line:confusing-naming
|
||||
func (m *memoryCache) refresh() (err error) {
|
||||
if m.events, err = getFutureEvents(m.dbc); err != nil {
|
||||
return errors.Wrap(err, "fetching events from database")
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// Package msgformat contains an API route to utilize the internal
|
||||
// message formatter to format strings
|
||||
package msgformat
|
||||
|
||||
import (
|
||||
|
@ -11,10 +13,11 @@ import (
|
|||
|
||||
var formatMessage plugins.MsgFormatter
|
||||
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
// Register provides the plugins.RegisterFunc
|
||||
func Register(args plugins.RegistrationArguments) (err error) {
|
||||
formatMessage = args.FormatMessage
|
||||
|
||||
args.RegisterAPIRoute(plugins.HTTPRouteRegistrationArgs{
|
||||
if err = args.RegisterAPIRoute(plugins.HTTPRouteRegistrationArgs{
|
||||
Description: "Takes the given template and renders it using the same renderer as messages in the channel are",
|
||||
HandlerFunc: handleFormattedMessage,
|
||||
Method: http.MethodGet,
|
||||
|
@ -31,7 +34,9 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
},
|
||||
RequiresWriteAuth: true, // This module can potentially be used to harvest data / read internal variables so it is handled as a write-module
|
||||
ResponseType: plugins.HTTPRouteResponseTypeTextPlain,
|
||||
})
|
||||
}); err != nil {
|
||||
return fmt.Errorf("registering API route: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/Luzifer/go_helpers/v2/backoff"
|
||||
"github.com/Luzifer/twitch-bot/v3/internal/helpers"
|
||||
"github.com/Luzifer/twitch-bot/v3/pkg/database"
|
||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||
|
@ -24,26 +25,29 @@ type (
|
|||
}
|
||||
)
|
||||
|
||||
func AddChannelEvent(db database.Connector, channel string, evt SocketMessage) error {
|
||||
func addChannelEvent(db database.Connector, channel string, evt socketMessage) (evtID uint64, err error) {
|
||||
buf := new(bytes.Buffer)
|
||||
if err := json.NewEncoder(buf).Encode(evt.Fields); err != nil {
|
||||
return errors.Wrap(err, "encoding fields")
|
||||
return 0, errors.Wrap(err, "encoding fields")
|
||||
}
|
||||
|
||||
return errors.Wrap(
|
||||
helpers.RetryTransaction(db.DB(), func(tx *gorm.DB) error {
|
||||
return tx.Create(&overlaysEvent{
|
||||
Channel: channel,
|
||||
CreatedAt: evt.Time.UTC(),
|
||||
EventType: evt.Type,
|
||||
Fields: strings.TrimSpace(buf.String()),
|
||||
}).Error
|
||||
}),
|
||||
"storing event to database",
|
||||
)
|
||||
storEvt := &overlaysEvent{
|
||||
Channel: channel,
|
||||
CreatedAt: evt.Time.UTC(),
|
||||
EventType: evt.Type,
|
||||
Fields: strings.TrimSpace(buf.String()),
|
||||
}
|
||||
|
||||
if err = helpers.RetryTransaction(db.DB(), func(tx *gorm.DB) error {
|
||||
return tx.Create(storEvt).Error
|
||||
}); err != nil {
|
||||
return 0, errors.Wrap(err, "storing event to database")
|
||||
}
|
||||
|
||||
return storEvt.ID, nil
|
||||
}
|
||||
|
||||
func GetChannelEvents(db database.Connector, channel string) ([]SocketMessage, error) {
|
||||
func getChannelEvents(db database.Connector, channel string) ([]socketMessage, error) {
|
||||
var evts []overlaysEvent
|
||||
|
||||
if err := helpers.Retry(func() error {
|
||||
|
@ -52,20 +56,46 @@ func GetChannelEvents(db database.Connector, channel string) ([]SocketMessage, e
|
|||
return nil, errors.Wrap(err, "querying channel events")
|
||||
}
|
||||
|
||||
var out []SocketMessage
|
||||
var out []socketMessage
|
||||
for _, e := range evts {
|
||||
fields := new(plugins.FieldCollection)
|
||||
if err := json.NewDecoder(strings.NewReader(e.Fields)).Decode(fields); err != nil {
|
||||
return nil, errors.Wrap(err, "decoding fields")
|
||||
sm, err := e.ToSocketMessage()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "transforming event")
|
||||
}
|
||||
|
||||
out = append(out, SocketMessage{
|
||||
IsLive: false,
|
||||
Time: e.CreatedAt,
|
||||
Type: e.EventType,
|
||||
Fields: fields,
|
||||
})
|
||||
out = append(out, sm)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func getEventByID(db database.Connector, eventID uint64) (socketMessage, error) {
|
||||
var evt overlaysEvent
|
||||
|
||||
if err := helpers.Retry(func() (err error) {
|
||||
err = db.DB().Where("id = ?", eventID).First(&evt).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return backoff.NewErrCannotRetry(err) //nolint:wrapcheck // we get our internal error
|
||||
}
|
||||
return err
|
||||
}); err != nil {
|
||||
return socketMessage{}, errors.Wrap(err, "fetching event")
|
||||
}
|
||||
|
||||
return evt.ToSocketMessage()
|
||||
}
|
||||
|
||||
func (o overlaysEvent) ToSocketMessage() (socketMessage, error) {
|
||||
fields := new(plugins.FieldCollection)
|
||||
if err := json.NewDecoder(strings.NewReader(o.Fields)).Decode(fields); err != nil {
|
||||
return socketMessage{}, errors.Wrap(err, "decoding fields")
|
||||
}
|
||||
|
||||
return socketMessage{
|
||||
EventID: o.ID,
|
||||
IsLive: false,
|
||||
Time: o.CreatedAt,
|
||||
Type: o.EventType,
|
||||
Fields: fields,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -17,38 +17,55 @@ func TestEventDatabaseRoundtrip(t *testing.T) {
|
|||
|
||||
var (
|
||||
channel = "#test"
|
||||
tEvent1 = time.Now()
|
||||
evtID uint64
|
||||
tEvent1 = time.Now().UTC()
|
||||
tEvent2 = tEvent1.Add(time.Second)
|
||||
)
|
||||
|
||||
evts, err := GetChannelEvents(dbc, channel)
|
||||
evts, err := getChannelEvents(dbc, channel)
|
||||
assert.NoError(t, err, "getting events on empty db")
|
||||
assert.Zero(t, evts, "expect no events on empty db")
|
||||
|
||||
assert.NoError(t, AddChannelEvent(dbc, channel, SocketMessage{
|
||||
evtID, err = addChannelEvent(dbc, channel, socketMessage{
|
||||
IsLive: true,
|
||||
Time: tEvent2,
|
||||
Type: "event 2",
|
||||
Fields: plugins.FieldCollectionFromData(map[string]any{"foo": "bar"}),
|
||||
}), "adding second event")
|
||||
})
|
||||
assert.Equal(t, uint64(1), evtID)
|
||||
assert.NoError(t, err, "adding second event")
|
||||
|
||||
assert.NoError(t, AddChannelEvent(dbc, channel, SocketMessage{
|
||||
evtID, err = addChannelEvent(dbc, channel, socketMessage{
|
||||
IsLive: true,
|
||||
Time: tEvent1,
|
||||
Type: "event 1",
|
||||
Fields: plugins.FieldCollectionFromData(map[string]any{"foo": "bar"}),
|
||||
}), "adding first event")
|
||||
})
|
||||
assert.Equal(t, uint64(2), evtID)
|
||||
assert.NoError(t, err, "adding first event")
|
||||
|
||||
assert.NoError(t, AddChannelEvent(dbc, "#otherchannel", SocketMessage{
|
||||
evtID, err = addChannelEvent(dbc, "#otherchannel", socketMessage{
|
||||
IsLive: true,
|
||||
Time: tEvent1,
|
||||
Type: "event",
|
||||
Fields: plugins.FieldCollectionFromData(map[string]any{"foo": "bar"}),
|
||||
}), "adding other channel event")
|
||||
})
|
||||
assert.Equal(t, uint64(3), evtID)
|
||||
assert.NoError(t, err, "adding other channel event")
|
||||
|
||||
evts, err = GetChannelEvents(dbc, channel)
|
||||
evts, err = getChannelEvents(dbc, channel)
|
||||
assert.NoError(t, err, "getting events")
|
||||
assert.Len(t, evts, 2, "expect 2 events")
|
||||
|
||||
assert.Less(t, evts[0].Time, evts[1].Time, "expect sorting")
|
||||
|
||||
evt, err := getEventByID(dbc, 2)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, socketMessage{
|
||||
EventID: 2,
|
||||
IsLive: false,
|
||||
Time: tEvent1,
|
||||
Type: "event 1",
|
||||
Fields: plugins.FieldCollectionFromData(map[string]any{"foo": "bar"}),
|
||||
}, evt)
|
||||
}
|
||||
|
|
29
internal/apimodules/overlays/dav.go
Normal file
29
internal/apimodules/overlays/dav.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package overlays
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/net/webdav"
|
||||
)
|
||||
|
||||
func getDAVHandler() http.HandlerFunc {
|
||||
overlaysDir := os.Getenv("OVERLAYS_DIR")
|
||||
if ds, err := os.Stat(overlaysDir); err != nil || overlaysDir == "" || !ds.IsDir() {
|
||||
return http.NotFound
|
||||
}
|
||||
|
||||
return (&webdav.Handler{
|
||||
Prefix: "/overlays/dav",
|
||||
FileSystem: webdav.Dir(overlaysDir),
|
||||
LockSystem: webdav.NewMemLS(),
|
||||
Logger: func(r *http.Request, err error) {
|
||||
logger := logrus.WithField("module", "overlays-dav")
|
||||
if err != nil {
|
||||
logger = logger.WithError(err)
|
||||
}
|
||||
logger.Debugf("%s %s", r.Method, r.URL)
|
||||
},
|
||||
}).ServeHTTP
|
||||
}
|
|
@ -30,9 +30,10 @@
|
|||
|
||||
<div id="app" v-cloak>
|
||||
<table>
|
||||
<tr><th>Time</th><th>Event</th><th>Fields</th></tr>
|
||||
<tr><th>Time</th><th>Reason</th><th>Event</th><th>Fields</th></tr>
|
||||
<tr v-for="event in events">
|
||||
<td>{{ moment(event.time).format('YYYY-MM-DD HH:mm:ss') }}</td>
|
||||
<td>{{ event.reason }}</td>
|
||||
<td>{{ event.event }}</td>
|
||||
<td>
|
||||
<span
|
||||
|
@ -73,13 +74,13 @@
|
|||
mounted() {
|
||||
window.botClient = new EventClient({
|
||||
handlers: {
|
||||
_: (evt, data, time, live) => {
|
||||
if (window.botClient.paramOptionFallback('hide', '').split(',').includes(evt)) {
|
||||
_: ({ fields, reason, time, type }) => {
|
||||
if (window.botClient.paramOptionFallback('hide', '').split(',').includes(type)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.events = [
|
||||
{ event: evt, fields: data, time },
|
||||
{ event: type, fields, reason, time },
|
||||
...this.events,
|
||||
]
|
||||
},
|
||||
|
|
|
@ -1,11 +1,22 @@
|
|||
/**
|
||||
* Options to pass to the EventClient constructor
|
||||
* @typedef {Object} Options
|
||||
* @prop {string} [channel] - Filter for specific channel events (format: `#channel`)
|
||||
* @prop {Object} [handlers={}] - Map event types to callback functions `(event, fields, time, live) => {...}`
|
||||
* @prop {number} [maxReplayAge=-1] - Number of hours to replay the events for (-1 = infinite)
|
||||
* @prop {boolean} [replay=false] - Request a replay at connect (requires channel to be set to a channel name)
|
||||
* @prop {string} [token] - API access token to use to connect to the WebSocket (if not set, must be provided through URL hash)
|
||||
* @prop {String} [channel] - Filter for specific channel events (format: `#channel`)
|
||||
* @prop {Object} [handlers={}] - Map event types to callback functions `(eventObj) => { ... }` (new) or `(event, fields, time, live) => {...}` (old)
|
||||
* @prop {Number} [maxReplayAge=-1] - Number of hours to replay the events for (-1 = infinite)
|
||||
* @prop {Boolean} [replay=false] - Request a replay at connect (requires channel to be set to a channel name)
|
||||
* @prop {String} [token] - API access token to use to connect to the WebSocket (if not set, must be provided through URL hash)
|
||||
*/
|
||||
|
||||
/**
|
||||
* SocketMessage received for every event and passed to the new `(eventObj) => { ... }` handlers
|
||||
* @typedef {Object} SocketMessage
|
||||
* @prop {Number} [event_id] - UID of the event used to re-trigger an event
|
||||
* @prop {Boolean} [is_live] - Whether the event was sent through a replay (false) or occurred live (true)
|
||||
* @prop {String} [reason] - Reason of this message (one of `bulk-replay`, `live-event`, `single-replay`)
|
||||
* @prop {String} [time] - RFC3339 timestamp of the event
|
||||
* @prop {String} [type] - Event type (i.e. `raid`, `sub`, ...)
|
||||
* @prop {Object} [fields] - string->any mapping of fields available for the event
|
||||
*/
|
||||
|
||||
const HOUR = 3600 * 1000
|
||||
|
@ -24,7 +35,7 @@ class EventClient {
|
|||
* @param {Options} opts Options for the EventClient
|
||||
*/
|
||||
constructor(opts) {
|
||||
this.params = new URLSearchParams(window.location.hash.substr(1))
|
||||
this.params = new URLSearchParams(window.location.hash.substring(1))
|
||||
this.handlers = { ...opts.handlers || {} }
|
||||
this.options = { ...opts }
|
||||
|
||||
|
@ -52,7 +63,7 @@ class EventClient {
|
|||
* @returns {string} API base URL
|
||||
*/
|
||||
apiBase() {
|
||||
return window.location.href.substr(0, window.location.href.indexOf('/overlays/'))
|
||||
return window.location.href.substring(0, window.location.href.indexOf('/overlays/'))
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -88,7 +99,7 @@ class EventClient {
|
|||
}
|
||||
|
||||
for (const fn of [this.handlers[data.type], this.handlers._].filter(fn => fn)) {
|
||||
fn(data.type, data.fields, new Date(data.time), data.is_live)
|
||||
fn.length === 1 ? fn({ ...data, time: new Date(data.time) }) : fn(data.type, data.fields, new Date(data.time), data.is_live)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -125,7 +136,7 @@ class EventClient {
|
|||
|
||||
for (const msg of data) {
|
||||
for (const fn of [this.handlers[msg.type], this.handlers._].filter(fn => fn)) {
|
||||
handlers.push(fn(msg.type, msg.fields, new Date(msg.time), msg.is_live))
|
||||
handlers.push(fn.length === 1 ? fn({ ...msg, time: new Date(msg.time) }) : fn(msg.type, msg.fields, new Date(msg.time), msg.is_live))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -159,6 +170,21 @@ class EventClient {
|
|||
.then(resp => resp.text())
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers a replay of the given event to all overlays currently listening for events. This event will have the `is_live` flag set to `false`.
|
||||
*
|
||||
* @param {Number} eventId The ID of the event received through the SocketMessage object
|
||||
* @returns {Promise} Promise of the fetch request
|
||||
*/
|
||||
replayEvent(eventId) {
|
||||
return fetch(`${this.apiBase()}/overlays/event/${eventId}/replay`, {
|
||||
headers: {
|
||||
authorization: this.paramOptionFallback('token'),
|
||||
},
|
||||
method: 'PUT',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies the overlay address to the websocket address the bot listens to
|
||||
*
|
||||
|
|
|
@ -8,11 +8,8 @@ new Vue({
|
|||
|
||||
new EventClient({
|
||||
handlers: {
|
||||
custom: (evt, data, time, live) => this.handleCustom(evt, data, time, live),
|
||||
custom: ({ fields }) => this.handleCustom(fields),
|
||||
},
|
||||
|
||||
maxReplayAge: 720,
|
||||
replay: true,
|
||||
})
|
||||
},
|
||||
|
||||
|
@ -64,14 +61,9 @@ new Vue({
|
|||
source.connect(preGainNode)
|
||||
},
|
||||
|
||||
handleCustom(evt, data, time, live) {
|
||||
handleCustom(data) {
|
||||
switch (data.type) {
|
||||
case 'soundalert':
|
||||
if (!live) {
|
||||
// Not a live event, do not issue alerts
|
||||
return
|
||||
}
|
||||
|
||||
this.queueAlert({
|
||||
soundUrl: data.soundUrl,
|
||||
})
|
||||
|
|
|
@ -12,8 +12,8 @@ var _ http.FileSystem = httpFSStack{}
|
|||
type httpFSStack []http.FileSystem
|
||||
|
||||
func (h httpFSStack) Open(name string) (http.File, error) {
|
||||
for _, fs := range h {
|
||||
if f, err := fs.Open(name); err == nil {
|
||||
for _, stackedFS := range h {
|
||||
if f, err := stackedFS.Open(name); err == nil {
|
||||
return f, nil
|
||||
}
|
||||
}
|
||||
|
@ -34,5 +34,5 @@ func newPrefixedFS(prefix string, originFS http.FileSystem) *prefixedFS {
|
|||
}
|
||||
|
||||
func (p prefixedFS) Open(name string) (http.File, error) {
|
||||
return p.originFS.Open(path.Join(p.prefix, name))
|
||||
return p.originFS.Open(path.Join(p.prefix, name)) //nolint:wrapcheck
|
||||
}
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
// Package overlays contains a server to host overlays and interact
|
||||
// with the bot using sockets and a pre-defined Javascript client
|
||||
package overlays
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
@ -31,14 +35,28 @@ const (
|
|||
)
|
||||
|
||||
type (
|
||||
SocketMessage struct {
|
||||
IsLive bool `json:"is_live"`
|
||||
Time time.Time `json:"time"`
|
||||
Type string `json:"type"`
|
||||
Fields *plugins.FieldCollection `json:"fields"`
|
||||
// sendReason contains an enum of reasons why the message is
|
||||
// transmitted to the listening overlay sockets
|
||||
sendReason string
|
||||
|
||||
// socketMessage represents the message overlay sockets will receive
|
||||
socketMessage struct {
|
||||
EventID uint64 `json:"event_id"`
|
||||
IsLive bool `json:"is_live"`
|
||||
Reason sendReason `json:"reason"`
|
||||
Time time.Time `json:"time"`
|
||||
Type string `json:"type"`
|
||||
Fields *plugins.FieldCollection `json:"fields"`
|
||||
}
|
||||
)
|
||||
|
||||
// Collection of SendReason entries
|
||||
const (
|
||||
sendReasonLive sendReason = "live-event"
|
||||
sendReasonBulkReplay sendReason = "bulk-replay"
|
||||
sendReasonSingleReplay sendReason = "single-replay"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed default/**
|
||||
embeddedOverlays embed.FS
|
||||
|
@ -53,7 +71,7 @@ var (
|
|||
"join", "part", // Those make no sense for replay
|
||||
}
|
||||
|
||||
subscribers = map[string]func(event string, eventData *plugins.FieldCollection){}
|
||||
subscribers = map[string]func(socketMessage){}
|
||||
subscribersLock sync.RWMutex
|
||||
|
||||
upgrader = websocket.Upgrader{
|
||||
|
@ -64,19 +82,40 @@ var (
|
|||
validateToken plugins.ValidateTokenFunc
|
||||
)
|
||||
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
// Register provides the plugins.RegisterFunc
|
||||
//
|
||||
//nolint:funlen
|
||||
func Register(args plugins.RegistrationArguments) (err error) {
|
||||
db = args.GetDatabaseConnector()
|
||||
if err := db.DB().AutoMigrate(&overlaysEvent{}); err != nil {
|
||||
if err = db.DB().AutoMigrate(&overlaysEvent{}); err != nil {
|
||||
return errors.Wrap(err, "applying schema migration")
|
||||
}
|
||||
|
||||
args.RegisterCopyDatabaseFunc("overlay_events", func(src, target *gorm.DB) error {
|
||||
return database.CopyObjects(src, target, &overlaysEvent{})
|
||||
return database.CopyObjects(src, target, &overlaysEvent{}) //nolint:wrapcheck // internal helper
|
||||
})
|
||||
|
||||
validateToken = args.ValidateToken
|
||||
|
||||
args.RegisterAPIRoute(plugins.HTTPRouteRegistrationArgs{
|
||||
if err = args.RegisterAPIRoute(plugins.HTTPRouteRegistrationArgs{
|
||||
Description: "Trigger a re-distribution of an event to all subscribed overlays",
|
||||
HandlerFunc: handleSingleEventReplay,
|
||||
Method: http.MethodPut,
|
||||
Module: "overlays",
|
||||
Name: "Replay Single Event",
|
||||
Path: "/event/{event_id}/replay",
|
||||
ResponseType: plugins.HTTPRouteResponseTypeNo200,
|
||||
RouteParams: []plugins.HTTPRouteParamDocumentation{
|
||||
{
|
||||
Description: "Event ID to replay (unique ID in database)",
|
||||
Name: "event_id",
|
||||
},
|
||||
},
|
||||
}); err != nil {
|
||||
return fmt.Errorf("registering API route: %w", err)
|
||||
}
|
||||
|
||||
if err = args.RegisterAPIRoute(plugins.HTTPRouteRegistrationArgs{
|
||||
Description: "Websocket subscriber for bot events",
|
||||
HandlerFunc: handleSocketSubscription,
|
||||
Method: http.MethodGet,
|
||||
|
@ -84,9 +123,11 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
Name: "Websocket",
|
||||
Path: "/events.sock",
|
||||
ResponseType: plugins.HTTPRouteResponseTypeMultiple,
|
||||
})
|
||||
}); err != nil {
|
||||
return fmt.Errorf("registering API route: %w", err)
|
||||
}
|
||||
|
||||
args.RegisterAPIRoute(plugins.HTTPRouteRegistrationArgs{
|
||||
if err = args.RegisterAPIRoute(plugins.HTTPRouteRegistrationArgs{
|
||||
Description: "Fetch past events for the given channel",
|
||||
HandlerFunc: handleEventsReplay,
|
||||
Method: http.MethodGet,
|
||||
|
@ -109,9 +150,25 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
Name: "channel",
|
||||
},
|
||||
},
|
||||
})
|
||||
}); err != nil {
|
||||
return fmt.Errorf("registering API route: %w", err)
|
||||
}
|
||||
|
||||
args.RegisterAPIRoute(plugins.HTTPRouteRegistrationArgs{
|
||||
if err = args.RegisterAPIRoute(plugins.HTTPRouteRegistrationArgs{
|
||||
Description: "Shares the overlays folder as WebDAV filesystem",
|
||||
HandlerFunc: getDAVHandler(),
|
||||
IsPrefix: true,
|
||||
Module: "overlays",
|
||||
Name: "WebDAV Overlays",
|
||||
Path: "/dav/",
|
||||
RequiresWriteAuth: true,
|
||||
ResponseType: plugins.HTTPRouteResponseTypeMultiple,
|
||||
SkipDocumentation: true,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("registering API route: %w", err)
|
||||
}
|
||||
|
||||
if err = args.RegisterAPIRoute(plugins.HTTPRouteRegistrationArgs{
|
||||
HandlerFunc: handleServeOverlayAsset,
|
||||
IsPrefix: true,
|
||||
Method: http.MethodGet,
|
||||
|
@ -119,30 +176,43 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
Path: "/",
|
||||
ResponseType: plugins.HTTPRouteResponseTypeMultiple,
|
||||
SkipDocumentation: true,
|
||||
})
|
||||
}); err != nil {
|
||||
return fmt.Errorf("registering API route: %w", err)
|
||||
}
|
||||
|
||||
args.RegisterEventHandler(func(event string, eventData *plugins.FieldCollection) error {
|
||||
if err = args.RegisterEventHandler(func(event string, eventData *plugins.FieldCollection) (err error) {
|
||||
subscribersLock.RLock()
|
||||
defer subscribersLock.RUnlock()
|
||||
|
||||
msg := socketMessage{
|
||||
IsLive: true,
|
||||
Reason: sendReasonLive,
|
||||
Time: time.Now(),
|
||||
Type: event,
|
||||
Fields: eventData,
|
||||
}
|
||||
|
||||
if msg.EventID, err = addChannelEvent(db, plugins.DeriveChannel(nil, eventData), socketMessage{
|
||||
IsLive: false,
|
||||
Time: time.Now(),
|
||||
Type: event,
|
||||
Fields: eventData,
|
||||
}); err != nil {
|
||||
return errors.Wrap(err, "storing event")
|
||||
}
|
||||
|
||||
for _, fn := range subscribers {
|
||||
fn(event, eventData)
|
||||
fn(msg)
|
||||
}
|
||||
|
||||
if str.StringInSlice(event, storeExemption) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.Wrap(
|
||||
AddChannelEvent(db, plugins.DeriveChannel(nil, eventData), SocketMessage{
|
||||
IsLive: false,
|
||||
Time: time.Now(),
|
||||
Type: event,
|
||||
Fields: eventData,
|
||||
}),
|
||||
"storing event",
|
||||
)
|
||||
})
|
||||
return nil
|
||||
}); err != nil {
|
||||
return fmt.Errorf("registering event handler: %w", err)
|
||||
}
|
||||
|
||||
fsStack = httpFSStack{
|
||||
newPrefixedFS("default", http.FS(embeddedOverlays)),
|
||||
|
@ -161,7 +231,7 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
func handleEventsReplay(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
channel = mux.Vars(r)["channel"]
|
||||
msgs []SocketMessage
|
||||
msgs []socketMessage
|
||||
since = time.Time{}
|
||||
)
|
||||
|
||||
|
@ -169,7 +239,7 @@ func handleEventsReplay(w http.ResponseWriter, r *http.Request) {
|
|||
since = s
|
||||
}
|
||||
|
||||
events, err := GetChannelEvents(db, "#"+strings.TrimLeft(channel, "#"))
|
||||
events, err := getChannelEvents(db, "#"+strings.TrimLeft(channel, "#"))
|
||||
if err != nil {
|
||||
http.Error(w, errors.Wrap(err, "getting channel events").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
@ -180,6 +250,7 @@ func handleEventsReplay(w http.ResponseWriter, r *http.Request) {
|
|||
continue
|
||||
}
|
||||
|
||||
msg.Reason = sendReasonBulkReplay
|
||||
msgs = append(msgs, msg)
|
||||
}
|
||||
|
||||
|
@ -195,6 +266,29 @@ func handleServeOverlayAsset(w http.ResponseWriter, r *http.Request) {
|
|||
http.StripPrefix("/overlays", http.FileServer(fsStack)).ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func handleSingleEventReplay(w http.ResponseWriter, r *http.Request) {
|
||||
eventID, err := strconv.ParseUint(mux.Vars(r)["event_id"], 10, 64)
|
||||
if err != nil {
|
||||
http.Error(w, errors.Wrap(err, "parsing event_id").Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
evt, err := getEventByID(db, eventID)
|
||||
if err != nil {
|
||||
http.Error(w, errors.Wrap(err, "fetching event").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
evt.Reason = sendReasonSingleReplay
|
||||
|
||||
subscribersLock.RLock()
|
||||
defer subscribersLock.RUnlock()
|
||||
|
||||
for _, fn := range subscribers {
|
||||
fn(evt)
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:funlen,gocognit,gocyclo // Not split in order to keep the socket logic in one place
|
||||
func handleSocketSubscription(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
|
@ -208,25 +302,18 @@ func handleSocketSubscription(w http.ResponseWriter, r *http.Request) {
|
|||
logger.WithError(err).Error("Unable to upgrade socket")
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
defer conn.Close() //nolint:errcheck // We don't really care about this
|
||||
|
||||
var (
|
||||
authTimeout = time.NewTimer(authTimeout)
|
||||
connLock = new(sync.Mutex)
|
||||
errC = make(chan error, 1)
|
||||
isAuthorized bool
|
||||
sendMsgC = make(chan SocketMessage, 1)
|
||||
sendMsgC = make(chan socketMessage, 1)
|
||||
)
|
||||
|
||||
// Register listener
|
||||
unsub := subscribeSocket(func(event string, eventData *plugins.FieldCollection) {
|
||||
sendMsgC <- SocketMessage{
|
||||
IsLive: true,
|
||||
Time: time.Now(),
|
||||
Type: event,
|
||||
Fields: eventData,
|
||||
}
|
||||
})
|
||||
unsub := subscribeSocket(func(msg socketMessage) { sendMsgC <- msg })
|
||||
defer unsub()
|
||||
|
||||
keepAlive := time.NewTicker(socketKeepAlive)
|
||||
|
@ -238,7 +325,7 @@ func handleSocketSubscription(w http.ResponseWriter, r *http.Request) {
|
|||
if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
|
||||
logger.WithError(err).Error("Unable to send ping message")
|
||||
connLock.Unlock()
|
||||
conn.Close()
|
||||
conn.Close() //nolint:errcheck,gosec
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -274,7 +361,7 @@ func handleSocketSubscription(w http.ResponseWriter, r *http.Request) {
|
|||
continue
|
||||
}
|
||||
|
||||
var recvMsg SocketMessage
|
||||
var recvMsg socketMessage
|
||||
if err = json.Unmarshal(p, &recvMsg); err != nil {
|
||||
errC <- errors.Wrap(err, "decoding message")
|
||||
return
|
||||
|
@ -295,7 +382,7 @@ func handleSocketSubscription(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
authTimeout.Stop()
|
||||
isAuthorized = true
|
||||
sendMsgC <- SocketMessage{
|
||||
sendMsgC <- socketMessage{
|
||||
IsLive: true,
|
||||
Time: time.Now(),
|
||||
Type: msgTypeRequestAuth,
|
||||
|
@ -315,10 +402,18 @@ func handleSocketSubscription(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
|
||||
case err := <-errC:
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Message processing caused error")
|
||||
var cErr *websocket.CloseError
|
||||
switch {
|
||||
case err == nil:
|
||||
// We use nil-error to close the connection
|
||||
|
||||
case errors.As(err, &cErr) && websocket.IsCloseError(cErr, websocket.CloseNormalClosure, websocket.CloseGoingAway):
|
||||
// We don't need to log when the remote closes the websocket gracefully
|
||||
|
||||
default:
|
||||
logger.WithError(err).Error("message processing caused error")
|
||||
}
|
||||
return // We use nil-error to close the connection
|
||||
return // All errors need to quit this function
|
||||
|
||||
case msg := <-sendMsgC:
|
||||
if !isAuthorized {
|
||||
|
@ -330,14 +425,14 @@ func handleSocketSubscription(w http.ResponseWriter, r *http.Request) {
|
|||
if err := conn.WriteJSON(msg); err != nil {
|
||||
logger.WithError(err).Error("Unable to send socket message")
|
||||
connLock.Unlock()
|
||||
conn.Close()
|
||||
conn.Close() //nolint:errcheck,gosec
|
||||
}
|
||||
connLock.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func subscribeSocket(fn func(event string, eventData *plugins.FieldCollection)) func() {
|
||||
func subscribeSocket(fn func(socketMessage)) func() {
|
||||
id := uuid.Must(uuid.NewV4()).String()
|
||||
|
||||
subscribersLock.Lock()
|
||||
|
|
|
@ -17,7 +17,7 @@ var ptrStrEmpty = ptrStr("")
|
|||
|
||||
func ptrStr(v string) *string { return &v }
|
||||
|
||||
func (a enterRaffleActor) Execute(_ *irc.Client, m *irc.Message, _ *plugins.Rule, evtData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
func (enterRaffleActor) Execute(_ *irc.Client, m *irc.Message, _ *plugins.Rule, evtData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
if m != nil || evtData.MustString("reward_id", ptrStrEmpty) == "" {
|
||||
return false, errors.New("enter-raffle actor is only supposed to act on channelpoint redeems")
|
||||
}
|
||||
|
@ -67,10 +67,10 @@ func (a enterRaffleActor) Execute(_ *irc.Client, m *irc.Message, _ *plugins.Rule
|
|||
)
|
||||
}
|
||||
|
||||
func (a enterRaffleActor) IsAsync() bool { return false }
|
||||
func (a enterRaffleActor) Name() string { return "enter-raffle" }
|
||||
func (enterRaffleActor) IsAsync() bool { return false }
|
||||
func (enterRaffleActor) Name() string { return "enter-raffle" }
|
||||
|
||||
func (a enterRaffleActor) Validate(_ plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
func (enterRaffleActor) Validate(_ plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||
keyword, err := attrs.String("keyword")
|
||||
if err != nil || keyword == "" {
|
||||
return errors.New("keyword must be non-empty string")
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package raffle
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -70,7 +71,7 @@ func handleRaffleEntry(m *irc.Message, channel, user string) error {
|
|||
return errors.Wrap(err, "getting twitch client for raffle")
|
||||
}
|
||||
|
||||
since, err := raffleChan.GetFollowDate(user, strings.TrimLeft(channel, "#"))
|
||||
since, err := raffleChan.GetFollowDate(context.Background(), user, strings.TrimLeft(channel, "#"))
|
||||
switch {
|
||||
case err == nil:
|
||||
doesFollow = since.Before(time.Now().Add(-r.MinFollowAge))
|
||||
|
|
|
@ -53,7 +53,9 @@ func pickWinnerFromRaffle(r raffle) (winner raffleEntry, err error) {
|
|||
|
||||
func (cryptRandSrc) Int63() int64 {
|
||||
var b [8]byte
|
||||
rand.Read(b[:])
|
||||
if _, err := rand.Read(b[:]); err != nil {
|
||||
return -1
|
||||
}
|
||||
// mask off sign bit to ensure positive number
|
||||
return int64(binary.LittleEndian.Uint64(b[:]) & (1<<63 - 1))
|
||||
}
|
||||
|
|
|
@ -45,9 +45,12 @@ func testGenerateRaffe() raffle {
|
|||
|
||||
func BenchmarkPickWinnerFromRaffle(b *testing.B) {
|
||||
tData := testGenerateRaffe()
|
||||
var err error
|
||||
|
||||
b.Run("pick", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
pickWinnerFromRaffle(tData)
|
||||
_, err = pickWinnerFromRaffle(tData)
|
||||
require.NoError(b, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ var (
|
|||
tcGetter func(string) (*twitch.Client, error)
|
||||
)
|
||||
|
||||
// Register provides the plugins.RegisterFunc
|
||||
func Register(args plugins.RegistrationArguments) (err error) {
|
||||
db = args.GetDatabaseConnector()
|
||||
if err := db.DB().AutoMigrate(&raffle{}, &raffleEntry{}); err != nil {
|
||||
|
@ -28,7 +29,7 @@ func Register(args plugins.RegistrationArguments) (err error) {
|
|||
}
|
||||
|
||||
args.RegisterCopyDatabaseFunc("raffle", func(src, target *gorm.DB) error {
|
||||
return database.CopyObjects(src, target, &raffle{}, &raffleEntry{})
|
||||
return database.CopyObjects(src, target, &raffle{}, &raffleEntry{}) //nolint:wrapcheck // internal helper
|
||||
})
|
||||
|
||||
dbc = newDBClient(db)
|
||||
|
|
|
@ -12,6 +12,7 @@ const (
|
|||
// Retry contains a standard set of configuration parameters for an
|
||||
// exponential backoff to be used throughout the bot
|
||||
func Retry(fn func() error) error {
|
||||
//nolint:wrapcheck
|
||||
return backoff.NewBackoff().
|
||||
WithMaxIterations(maxRetries).
|
||||
Retry(fn)
|
||||
|
@ -21,5 +22,7 @@ func Retry(fn func() error) error {
|
|||
// the database. The function will be run in a transaction on the
|
||||
// database and will be retried as if executed using Retry
|
||||
func RetryTransaction(db *gorm.DB, fn func(tx *gorm.DB) error) error {
|
||||
return Retry(func() error { return db.Transaction(fn) })
|
||||
return Retry(func() error {
|
||||
return db.Transaction(fn) //nolint:wrapcheck
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// Package linkcheck implements a helper library to search for links
|
||||
// in a message text and validate them by trying to call them
|
||||
package linkcheck
|
||||
|
||||
import (
|
||||
|
@ -52,7 +54,7 @@ func (c Checker) ScanForLinks(message string) (links []string) {
|
|||
return c.scan(message, c.scanPlainNoObfuscate)
|
||||
}
|
||||
|
||||
func (c Checker) scan(message string, scanFns ...func(string) []string) (links []string) {
|
||||
func (Checker) scan(message string, scanFns ...func(string) []string) (links []string) {
|
||||
for _, scanner := range scanFns {
|
||||
if links = scanner(message); links != nil {
|
||||
return links
|
||||
|
@ -87,7 +89,6 @@ func (c Checker) scanPartsConnected(parts []string, connector string) (links []s
|
|||
|
||||
for ptJoin := 2; ptJoin < len(parts); ptJoin++ {
|
||||
for i := 0; i <= len(parts)-ptJoin; i++ {
|
||||
wg.Add(1)
|
||||
c.res.Resolve(resolverQueueEntry{
|
||||
Link: strings.Join(parts[i:i+ptJoin], connector),
|
||||
Callback: func(link string) { links = str.AppendIfMissing(links, link) },
|
||||
|
@ -108,7 +109,6 @@ func (c Checker) scanPlainNoObfuscate(message string) (links []string) {
|
|||
)
|
||||
|
||||
for _, part := range parts {
|
||||
wg.Add(1)
|
||||
c.res.Resolve(resolverQueueEntry{
|
||||
Link: part,
|
||||
Callback: func(link string) { links = str.AppendIfMissing(links, link) },
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/Luzifer/go_helpers/v2/str"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
@ -59,9 +60,10 @@ func TestScanForLinks(t *testing.T) {
|
|||
c := New()
|
||||
|
||||
for _, testCase := range []struct {
|
||||
Heuristic bool
|
||||
Message string
|
||||
ExpectedLinks []string
|
||||
Heuristic bool
|
||||
Message string
|
||||
ExpectedLinks []string
|
||||
ExpectedContains bool
|
||||
}{
|
||||
// Case: full URL is present in the message
|
||||
{
|
||||
|
@ -102,6 +104,7 @@ func TestScanForLinks(t *testing.T) {
|
|||
ExpectedLinks: []string{
|
||||
"http://example.com",
|
||||
},
|
||||
ExpectedContains: true,
|
||||
},
|
||||
// Case: link is obfuscated using space and braces
|
||||
{
|
||||
|
@ -110,6 +113,7 @@ func TestScanForLinks(t *testing.T) {
|
|||
ExpectedLinks: []string{
|
||||
"http://example.com",
|
||||
},
|
||||
ExpectedContains: true,
|
||||
},
|
||||
// Case: multiple links in one message
|
||||
{
|
||||
|
@ -162,9 +166,10 @@ func TestScanForLinks(t *testing.T) {
|
|||
},
|
||||
// Case: Multiple spaces in the link
|
||||
{
|
||||
Heuristic: true,
|
||||
Message: "Hey there, see my new project on exa mpl e. com! Get it fast now!",
|
||||
ExpectedLinks: []string{"http://example.com"},
|
||||
Heuristic: true,
|
||||
Message: "Hey there, see my new project on exa mpl e. com! Get it fast now!",
|
||||
ExpectedLinks: []string{"http://example.com"},
|
||||
ExpectedContains: true,
|
||||
},
|
||||
// Case: Dot in the end of the link with space
|
||||
{
|
||||
|
@ -187,7 +192,21 @@ func TestScanForLinks(t *testing.T) {
|
|||
}
|
||||
sort.Strings(linksFound)
|
||||
|
||||
assert.Equal(t, testCase.ExpectedLinks, linksFound, "links from message %q", testCase.Message)
|
||||
if testCase.ExpectedContains {
|
||||
for _, expLnk := range testCase.ExpectedLinks {
|
||||
assert.Contains(t, linksFound, expLnk)
|
||||
}
|
||||
|
||||
var extraLinks []string
|
||||
for _, link := range linksFound {
|
||||
if !str.StringInSlice(link, testCase.ExpectedLinks) {
|
||||
extraLinks = append(extraLinks, link)
|
||||
}
|
||||
}
|
||||
t.Logf("extra links found: %v", extraLinks)
|
||||
} else {
|
||||
assert.Equal(t, testCase.ExpectedLinks, linksFound)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/Luzifer/go_helpers/v2/str"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -74,6 +75,7 @@ func withSkipVerify() func(*resolver) {
|
|||
}
|
||||
|
||||
func (r resolver) Resolve(qe resolverQueueEntry) {
|
||||
qe.WaitGroup.Add(1)
|
||||
r.resolverC <- qe
|
||||
}
|
||||
|
||||
|
@ -84,6 +86,8 @@ func (resolver) getJar() *cookiejar.Jar {
|
|||
|
||||
// resolveFinal takes a link and looks up the final destination of
|
||||
// that link after all redirects were followed
|
||||
//
|
||||
//nolint:gocyclo
|
||||
func (r resolver) resolveFinal(link string, cookieJar *cookiejar.Jar, callStack []string, userAgent string) string {
|
||||
if !linkTest.MatchString(link) && !r.skipValidation {
|
||||
return ""
|
||||
|
@ -138,7 +142,11 @@ func (r resolver) resolveFinal(link string, cookieJar *cookiejar.Jar, callStack
|
|||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
logrus.WithError(err).Error("closing response body (leaked fd)")
|
||||
}
|
||||
}()
|
||||
|
||||
if resp.StatusCode > 299 && resp.StatusCode < 400 {
|
||||
// We got a redirect
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
// Package access contains a service to manage Twitch tokens and scopes
|
||||
package access
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
@ -21,6 +23,8 @@ const (
|
|||
)
|
||||
|
||||
type (
|
||||
// ClientConfig contains a configuration to derive new Twitch clients
|
||||
// from
|
||||
ClientConfig struct {
|
||||
TwitchClient string
|
||||
TwitchClientSecret string
|
||||
|
@ -37,11 +41,15 @@ type (
|
|||
Scopes string
|
||||
}
|
||||
|
||||
// Service manages the permission database
|
||||
Service struct{ db database.Connector }
|
||||
)
|
||||
|
||||
// ErrChannelNotAuthorized denotes there is no valid authoriztion for
|
||||
// the given channel
|
||||
var ErrChannelNotAuthorized = errors.New("channel is not authorized")
|
||||
|
||||
// New creates a new Service on the given database
|
||||
func New(db database.Connector) (*Service, error) {
|
||||
return &Service{db}, errors.Wrap(
|
||||
db.DB().AutoMigrate(&extendedPermission{}),
|
||||
|
@ -49,15 +57,18 @@ func New(db database.Connector) (*Service, error) {
|
|||
)
|
||||
}
|
||||
|
||||
func (s *Service) CopyDatabase(src, target *gorm.DB) error {
|
||||
return database.CopyObjects(src, target, &extendedPermission{})
|
||||
// CopyDatabase enables the bot to migrate the access database
|
||||
func (*Service) CopyDatabase(src, target *gorm.DB) error {
|
||||
return database.CopyObjects(src, target, &extendedPermission{}) //nolint:wrapcheck // Internal helper
|
||||
}
|
||||
|
||||
// GetBotUsername gets the cached bot username
|
||||
func (s Service) GetBotUsername() (botUsername string, err error) {
|
||||
err = s.db.ReadCoreMeta(coreMetaKeyBotUsername, &botUsername)
|
||||
return botUsername, errors.Wrap(err, "reading bot username")
|
||||
}
|
||||
|
||||
// GetChannelPermissions returns the scopes granted for the given channel
|
||||
func (s Service) GetChannelPermissions(channel string) ([]string, error) {
|
||||
var (
|
||||
err error
|
||||
|
@ -78,6 +89,8 @@ func (s Service) GetChannelPermissions(channel string) ([]string, error) {
|
|||
return strings.Split(perm.Scopes, " "), nil
|
||||
}
|
||||
|
||||
// GetBotTwitchClient returns a twitch.Client configured to act as the
|
||||
// bot user
|
||||
func (s Service) GetBotTwitchClient(cfg ClientConfig) (*twitch.Client, error) {
|
||||
botUsername, err := s.GetBotUsername()
|
||||
switch {
|
||||
|
@ -118,7 +131,7 @@ func (s Service) GetBotTwitchClient(cfg ClientConfig) (*twitch.Client, error) {
|
|||
// can determine who the bot is. That means we can set the username
|
||||
// for later reference and afterwards delete the duplicated tokens.
|
||||
|
||||
_, botUser, err := twitch.New(cfg.TwitchClient, cfg.TwitchClientSecret, botAccessToken, botRefreshToken).GetAuthorizedUser()
|
||||
_, botUser, err := twitch.New(cfg.TwitchClient, cfg.TwitchClientSecret, botAccessToken, botRefreshToken).GetAuthorizedUser(context.Background())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "validating stored access token")
|
||||
}
|
||||
|
@ -148,6 +161,8 @@ func (s Service) GetBotTwitchClient(cfg ClientConfig) (*twitch.Client, error) {
|
|||
return s.GetTwitchClientForChannel(botUser, cfg)
|
||||
}
|
||||
|
||||
// GetTwitchClientForChannel returns a twitch.Client configured to act
|
||||
// as the owner of the given channel
|
||||
func (s Service) GetTwitchClientForChannel(channel string, cfg ClientConfig) (*twitch.Client, error) {
|
||||
var (
|
||||
err error
|
||||
|
@ -157,7 +172,7 @@ func (s Service) GetTwitchClientForChannel(channel string, cfg ClientConfig) (*t
|
|||
if err = helpers.Retry(func() error {
|
||||
err = s.db.DB().First(&perm, "channel = ?", strings.TrimLeft(channel, "#")).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return backoff.NewErrCannotRetry(ErrChannelNotAuthorized)
|
||||
return backoff.NewErrCannotRetry(ErrChannelNotAuthorized) //nolint:wrapcheck // We get our own error
|
||||
}
|
||||
return errors.Wrap(err, "getting twitch credential from database")
|
||||
}); err != nil {
|
||||
|
@ -189,6 +204,8 @@ func (s Service) GetTwitchClientForChannel(channel string, cfg ClientConfig) (*t
|
|||
return tc, nil
|
||||
}
|
||||
|
||||
// HasAnyPermissionForChannel checks whether any of the given scopes
|
||||
// are granted for the given channel
|
||||
func (s Service) HasAnyPermissionForChannel(channel string, scopes ...string) (bool, error) {
|
||||
storedScopes, err := s.GetChannelPermissions(channel)
|
||||
if err != nil {
|
||||
|
@ -204,6 +221,8 @@ func (s Service) HasAnyPermissionForChannel(channel string, scopes ...string) (b
|
|||
return false, nil
|
||||
}
|
||||
|
||||
// HasPermissionsForChannel checks whether all of the given scopes
|
||||
// are granted for the given channel
|
||||
func (s Service) HasPermissionsForChannel(channel string, scopes ...string) (bool, error) {
|
||||
storedScopes, err := s.GetChannelPermissions(channel)
|
||||
if err != nil {
|
||||
|
@ -232,7 +251,7 @@ func (s Service) HasTokensForChannel(channel string) (bool, error) {
|
|||
if err = helpers.Retry(func() error {
|
||||
err = s.db.DB().First(&perm, "channel = ?", strings.TrimLeft(channel, "#")).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return backoff.NewErrCannotRetry(ErrChannelNotAuthorized)
|
||||
return backoff.NewErrCannotRetry(ErrChannelNotAuthorized) //nolint:wrapcheck // We'll get our own error
|
||||
}
|
||||
return errors.Wrap(err, "getting twitch credential from database")
|
||||
}); err != nil {
|
||||
|
@ -253,12 +272,14 @@ func (s Service) HasTokensForChannel(channel string) (bool, error) {
|
|||
return perm.AccessToken != "" && perm.RefreshToken != "", nil
|
||||
}
|
||||
|
||||
// ListPermittedChannels returns a list of all channels having a token
|
||||
// for the channels owner
|
||||
func (s Service) ListPermittedChannels() (out []string, err error) {
|
||||
var perms []extendedPermission
|
||||
if err = helpers.Retry(func() error {
|
||||
return errors.Wrap(s.db.DB().Find(&perms).Error, "listing permissions")
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
return nil, err //nolint:wrapcheck // is already wrapped on the inside
|
||||
}
|
||||
|
||||
for _, perm := range perms {
|
||||
|
@ -268,6 +289,7 @@ func (s Service) ListPermittedChannels() (out []string, err error) {
|
|||
return out, nil
|
||||
}
|
||||
|
||||
// RemoveAllExtendedTwitchCredentials wipes the access database
|
||||
func (s Service) RemoveAllExtendedTwitchCredentials() error {
|
||||
return errors.Wrap(
|
||||
helpers.RetryTransaction(s.db.DB(), func(tx *gorm.DB) error {
|
||||
|
@ -277,6 +299,8 @@ func (s Service) RemoveAllExtendedTwitchCredentials() error {
|
|||
)
|
||||
}
|
||||
|
||||
// RemoveExendedTwitchCredentials wipes the access database for a given
|
||||
// channel
|
||||
func (s Service) RemoveExendedTwitchCredentials(channel string) error {
|
||||
return errors.Wrap(
|
||||
helpers.RetryTransaction(s.db.DB(), func(tx *gorm.DB) error {
|
||||
|
@ -286,6 +310,7 @@ func (s Service) RemoveExendedTwitchCredentials(channel string) error {
|
|||
)
|
||||
}
|
||||
|
||||
// SetBotUsername stores the username of the bot
|
||||
func (s Service) SetBotUsername(channel string) (err error) {
|
||||
return errors.Wrap(
|
||||
s.db.StoreCoreMeta(coreMetaKeyBotUsername, strings.TrimLeft(channel, "#")),
|
||||
|
@ -293,6 +318,8 @@ func (s Service) SetBotUsername(channel string) (err error) {
|
|||
)
|
||||
}
|
||||
|
||||
// SetExtendedTwitchCredentials stores tokens and scopes for the given
|
||||
// channel into the access database
|
||||
func (s Service) SetExtendedTwitchCredentials(channel, accessToken, refreshToken string, scope []string) (err error) {
|
||||
if accessToken, err = s.db.EncryptField(accessToken); err != nil {
|
||||
return errors.Wrap(err, "encrypting access token")
|
||||
|
|
|
@ -13,15 +13,17 @@ import (
|
|||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const NegativeCacheTime = 5 * time.Minute
|
||||
const negativeCacheTime = 5 * time.Minute
|
||||
|
||||
type (
|
||||
// Service manages the cached auth results
|
||||
Service struct {
|
||||
backends []AuthFunc
|
||||
cache map[string]*CacheEntry
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// CacheEntry represents an entry in the cache Service
|
||||
CacheEntry struct {
|
||||
AuthResult error // Allows for negative caching
|
||||
ExpiresAt time.Time
|
||||
|
@ -40,6 +42,8 @@ type (
|
|||
// auth method and therefore is not an user
|
||||
var ErrUnauthorized = errors.New("unauthorized")
|
||||
|
||||
// New creates a new Service with the given backend methods to
|
||||
// authenticate users
|
||||
func New(backends ...AuthFunc) *Service {
|
||||
s := &Service{
|
||||
backends: backends,
|
||||
|
@ -50,6 +54,8 @@ func New(backends ...AuthFunc) *Service {
|
|||
return s
|
||||
}
|
||||
|
||||
// ValidateTokenFor checks backends whether the given token has access
|
||||
// to the given modules and caches the result
|
||||
func (s *Service) ValidateTokenFor(token string, modules ...string) error {
|
||||
s.lock.RLock()
|
||||
cached := s.cache[s.cacheKey(token)]
|
||||
|
@ -84,7 +90,7 @@ backendLoop:
|
|||
// user. Both should be cached. The error for a static time, the
|
||||
// valid result for the time given by the backend.
|
||||
if errors.Is(ce.AuthResult, ErrUnauthorized) {
|
||||
ce.ExpiresAt = time.Now().Add(NegativeCacheTime)
|
||||
ce.ExpiresAt = time.Now().Add(negativeCacheTime)
|
||||
}
|
||||
|
||||
s.lock.Lock()
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// Package timer contains a service to store and manage timers in a database
|
||||
package timer
|
||||
|
||||
import (
|
||||
|
@ -19,6 +20,7 @@ import (
|
|||
)
|
||||
|
||||
type (
|
||||
// Service implements a timer service
|
||||
Service struct {
|
||||
db database.Connector
|
||||
permitTimeout time.Duration
|
||||
|
@ -32,6 +34,7 @@ type (
|
|||
|
||||
var _ plugins.TimerStore = (*Service)(nil)
|
||||
|
||||
// New creates a new Service
|
||||
func New(db database.Connector, cronService *cron.Cron) (*Service, error) {
|
||||
s := &Service{
|
||||
db: db,
|
||||
|
@ -46,20 +49,24 @@ func New(db database.Connector, cronService *cron.Cron) (*Service, error) {
|
|||
return s, errors.Wrap(s.db.DB().AutoMigrate(&timer{}), "applying migrations")
|
||||
}
|
||||
|
||||
func (s *Service) CopyDatabase(src, target *gorm.DB) error {
|
||||
return database.CopyObjects(src, target, &timer{})
|
||||
// CopyDatabase enables the service to migrate to a new database
|
||||
func (*Service) CopyDatabase(src, target *gorm.DB) error {
|
||||
return database.CopyObjects(src, target, &timer{}) //nolint:wrapcheck // Helper in own package
|
||||
}
|
||||
|
||||
// UpdatePermitTimeout sets a new permit timeout for future permits
|
||||
func (s *Service) UpdatePermitTimeout(d time.Duration) {
|
||||
s.permitTimeout = d
|
||||
}
|
||||
|
||||
// Cooldown timer
|
||||
|
||||
// AddCooldown adds a new cooldown timer
|
||||
func (s Service) AddCooldown(tt plugins.TimerType, limiter, ruleID string, expiry time.Time) error {
|
||||
return s.SetTimer(s.getCooldownTimerKey(tt, limiter, ruleID), expiry)
|
||||
}
|
||||
|
||||
// InCooldown checks whether the cooldown has expired
|
||||
func (s Service) InCooldown(tt plugins.TimerType, limiter, ruleID string) (bool, error) {
|
||||
return s.HasTimer(s.getCooldownTimerKey(tt, limiter, ruleID))
|
||||
}
|
||||
|
@ -72,10 +79,12 @@ func (Service) getCooldownTimerKey(tt plugins.TimerType, limiter, ruleID string)
|
|||
|
||||
// Permit timer
|
||||
|
||||
// AddPermit adds a new permit timer
|
||||
func (s Service) AddPermit(channel, username string) error {
|
||||
return s.SetTimer(s.getPermitTimerKey(channel, username), time.Now().Add(s.permitTimeout))
|
||||
}
|
||||
|
||||
// HasPermit checks whether a valid permit is present
|
||||
func (s Service) HasPermit(channel, username string) (bool, error) {
|
||||
return s.HasTimer(s.getPermitTimerKey(channel, username))
|
||||
}
|
||||
|
@ -88,12 +97,13 @@ func (Service) getPermitTimerKey(channel, username string) string {
|
|||
|
||||
// Generic timer
|
||||
|
||||
// HasTimer checks whether a timer with given ID is present
|
||||
func (s Service) HasTimer(id string) (bool, error) {
|
||||
var t timer
|
||||
err := helpers.Retry(func() error {
|
||||
err := s.db.DB().First(&t, "id = ? AND expires_at >= ?", id, time.Now().UTC()).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return backoff.NewErrCannotRetry(err)
|
||||
return backoff.NewErrCannotRetry(err) //nolint:wrapcheck // We'll get our own error
|
||||
}
|
||||
return err
|
||||
})
|
||||
|
@ -109,6 +119,7 @@ func (s Service) HasTimer(id string) (bool, error) {
|
|||
}
|
||||
}
|
||||
|
||||
// SetTimer sets a timer with given ID and expiry
|
||||
func (s Service) SetTimer(id string, expiry time.Time) error {
|
||||
return errors.Wrap(
|
||||
helpers.RetryTransaction(s.db.DB(), func(tx *gorm.DB) error {
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
// Package api contains helpers to interact with remote APIs in templates
|
||||
package api
|
||||
|
||||
import "github.com/Luzifer/twitch-bot/v3/plugins"
|
||||
|
||||
// Register provides the plugins.RegisterFunc
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
args.RegisterTemplateFunction("jsonAPI", plugins.GenericTemplateFunctionGetter(jsonAPI), plugins.TemplateFuncDocumentation{
|
||||
Description: "Fetches remote URL and applies jq-like query to it returning the result as string. (Remote API needs to return status 200 within 5 seconds.)",
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
|
||||
"github.com/itchyny/gojq"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -41,7 +42,11 @@ func jsonAPI(uri, path string, fallback ...string) (string, error) {
|
|||
if err != nil {
|
||||
return "", errors.Wrap(err, "executing request")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
logrus.WithError(err).Error("closing response body (leaked fd)")
|
||||
}
|
||||
}()
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"net/url"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func textAPI(uri string, fallback ...string) (string, error) {
|
||||
|
@ -29,7 +30,11 @@ func textAPI(uri string, fallback ...string) (string, error) {
|
|||
if err != nil {
|
||||
return "", errors.Wrap(err, "executing request")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
logrus.WithError(err).Error("closing response body (leaked fd)")
|
||||
}
|
||||
}()
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// Package numeric contains helpers for numeric manipulation
|
||||
package numeric
|
||||
|
||||
import (
|
||||
|
@ -6,6 +7,7 @@ import (
|
|||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||
)
|
||||
|
||||
// Register provides the plugins.RegisterFunc
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
args.RegisterTemplateFunction("pow", plugins.GenericTemplateFunctionGetter(math.Pow), plugins.TemplateFuncDocumentation{
|
||||
Description: "Returns float from calculation: `float1 ** float2`",
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// Package random contains helpers to aid with randomness
|
||||
package random
|
||||
|
||||
import (
|
||||
|
@ -11,6 +12,7 @@ import (
|
|||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||
)
|
||||
|
||||
// Register provides the plugins.RegisterFunc
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
args.RegisterTemplateFunction("randomString", plugins.GenericTemplateFunctionGetter(randomString), plugins.TemplateFuncDocumentation{
|
||||
Description: "Randomly picks a string from a list of strings",
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// Package slice contains slice manipulation helpers
|
||||
package slice
|
||||
|
||||
import (
|
||||
|
@ -5,6 +6,7 @@ import (
|
|||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||
)
|
||||
|
||||
// Register provides the plugins.RegisterFunc
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
args.RegisterTemplateFunction("inList", plugins.GenericTemplateFunctionGetter(func(search string, list ...string) bool {
|
||||
return str.StringInSlice(search, list)
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// Package strings contains string manipulation helpers
|
||||
package strings
|
||||
|
||||
import (
|
||||
|
@ -8,6 +9,7 @@ import (
|
|||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||
)
|
||||
|
||||
// Register provides the plugins.RegisterFunc
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
args.RegisterTemplateFunction("b64urlenc", plugins.GenericTemplateFunctionGetter(base64URLEncode), plugins.TemplateFuncDocumentation{
|
||||
Description: "Encodes the input using base64 URL-encoding (like `b64enc` but using `URLEncoding` instead of `StdEncoding`)",
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// Package subscriber contains template functions to fetch sub-count
|
||||
// and -points
|
||||
package subscriber
|
||||
|
||||
import (
|
||||
|
@ -15,6 +17,7 @@ var (
|
|||
tcGetter func(string) (*twitch.Client, error)
|
||||
)
|
||||
|
||||
// Register provides the plugins.RegisterFunc
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
permCheckFn = args.HasPermissionForChannel
|
||||
tcGetter = args.GetTwitchClientForChannel
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package twitch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
||||
|
@ -38,7 +39,7 @@ func tplTwitchDoesFollowLongerThan(args plugins.RegistrationArguments) {
|
|||
return false, errors.Errorf("unexpected input for duration %t", t)
|
||||
}
|
||||
|
||||
fd, err := args.GetTwitchClient().GetFollowDate(from, to)
|
||||
fd, err := args.GetTwitchClient().GetFollowDate(context.Background(), from, to)
|
||||
switch {
|
||||
case err == nil:
|
||||
return time.Since(fd) > age, nil
|
||||
|
@ -61,7 +62,7 @@ func tplTwitchDoesFollowLongerThan(args plugins.RegistrationArguments) {
|
|||
|
||||
func tplTwitchDoesFollow(args plugins.RegistrationArguments) {
|
||||
args.RegisterTemplateFunction("doesFollow", plugins.GenericTemplateFunctionGetter(func(from, to string) (bool, error) {
|
||||
_, err := args.GetTwitchClient().GetFollowDate(from, to)
|
||||
_, err := args.GetTwitchClient().GetFollowDate(context.Background(), from, to)
|
||||
switch {
|
||||
case err == nil:
|
||||
return true, nil
|
||||
|
@ -84,7 +85,7 @@ func tplTwitchDoesFollow(args plugins.RegistrationArguments) {
|
|||
|
||||
func tplTwitchFollowAge(args plugins.RegistrationArguments) {
|
||||
args.RegisterTemplateFunction("followAge", plugins.GenericTemplateFunctionGetter(func(from, to string) (time.Duration, error) {
|
||||
since, err := args.GetTwitchClient().GetFollowDate(from, to)
|
||||
since, err := args.GetTwitchClient().GetFollowDate(context.Background(), from, to)
|
||||
return time.Since(since), errors.Wrap(err, "getting follow date")
|
||||
}), plugins.TemplateFuncDocumentation{
|
||||
Description: "Looks up when `from` followed `to` and returns the duration between then and now (the bot must be moderator of `to` to read this)",
|
||||
|
@ -98,7 +99,8 @@ func tplTwitchFollowAge(args plugins.RegistrationArguments) {
|
|||
|
||||
func tplTwitchFollowDate(args plugins.RegistrationArguments) {
|
||||
args.RegisterTemplateFunction("followDate", plugins.GenericTemplateFunctionGetter(func(from, to string) (time.Time, error) {
|
||||
return args.GetTwitchClient().GetFollowDate(from, to)
|
||||
fd, err := args.GetTwitchClient().GetFollowDate(context.Background(), from, to)
|
||||
return fd, errors.Wrap(err, "getting follow date")
|
||||
}), plugins.TemplateFuncDocumentation{
|
||||
Description: "Looks up when `from` followed `to` (the bot must be moderator of `to` to read this)",
|
||||
Syntax: "followDate <from> <to>",
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
package twitch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -18,12 +21,12 @@ func init() {
|
|||
|
||||
func tplTwitchRecentGame(args plugins.RegistrationArguments) {
|
||||
args.RegisterTemplateFunction("recentGame", plugins.GenericTemplateFunctionGetter(func(username string, v ...string) (string, error) {
|
||||
game, _, err := args.GetTwitchClient().GetRecentStreamInfo(strings.TrimLeft(username, "#"))
|
||||
game, _, err := args.GetTwitchClient().GetRecentStreamInfo(context.Background(), strings.TrimLeft(username, "#"))
|
||||
if len(v) > 0 && (err != nil || game == "") {
|
||||
return v[0], nil
|
||||
return v[0], nil //nolint:nilerr // This is a default fallback
|
||||
}
|
||||
|
||||
return game, err
|
||||
return game, errors.Wrap(err, "getting stream info")
|
||||
}), plugins.TemplateFuncDocumentation{
|
||||
Description: "Returns the last played game name of the specified user (see shoutout example) or the `fallback` if the game could not be fetched. If no fallback was supplied the message will fail and not be sent.",
|
||||
Syntax: "recentGame <username> [fallback]",
|
||||
|
@ -36,12 +39,12 @@ func tplTwitchRecentGame(args plugins.RegistrationArguments) {
|
|||
|
||||
func tplTwitchRecentTitle(args plugins.RegistrationArguments) {
|
||||
args.RegisterTemplateFunction("recentTitle", plugins.GenericTemplateFunctionGetter(func(username string, v ...string) (string, error) {
|
||||
_, title, err := args.GetTwitchClient().GetRecentStreamInfo(strings.TrimLeft(username, "#"))
|
||||
_, title, err := args.GetTwitchClient().GetRecentStreamInfo(context.Background(), strings.TrimLeft(username, "#"))
|
||||
if len(v) > 0 && (err != nil || title == "") {
|
||||
return v[0], nil
|
||||
return v[0], nil //nolint:nilerr // This is a default fallback
|
||||
}
|
||||
|
||||
return title, err
|
||||
return title, errors.Wrap(err, "getting stream info")
|
||||
}), plugins.TemplateFuncDocumentation{
|
||||
Description: "Returns the last stream title of the specified user or the `fallback` if the title could not be fetched. If no fallback was supplied the message will fail and not be sent.",
|
||||
Syntax: "recentTitle <username> [fallback]",
|
||||
|
@ -54,9 +57,9 @@ func tplTwitchRecentTitle(args plugins.RegistrationArguments) {
|
|||
|
||||
func tplTwitchStreamUptime(args plugins.RegistrationArguments) {
|
||||
args.RegisterTemplateFunction("streamUptime", plugins.GenericTemplateFunctionGetter(func(username string) (time.Duration, error) {
|
||||
si, err := args.GetTwitchClient().GetCurrentStreamInfo(strings.TrimLeft(username, "#"))
|
||||
si, err := args.GetTwitchClient().GetCurrentStreamInfo(context.Background(), strings.TrimLeft(username, "#"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return 0, fmt.Errorf("getting stream info: %w", err)
|
||||
}
|
||||
return time.Since(si.StartedAt), nil
|
||||
}), plugins.TemplateFuncDocumentation{
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
|
||||
var regFn []func(plugins.RegistrationArguments)
|
||||
|
||||
// Register provides the plugins.RegisterFunc
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
for _, fn := range regFn {
|
||||
fn(args)
|
||||
|
|
|
@ -20,12 +20,12 @@ func init() {
|
|||
|
||||
func tplTwitchDisplayName(args plugins.RegistrationArguments) {
|
||||
args.RegisterTemplateFunction("displayName", plugins.GenericTemplateFunctionGetter(func(username string, v ...string) (string, error) {
|
||||
displayName, err := args.GetTwitchClient().GetDisplayNameForUser(strings.TrimLeft(username, "#"))
|
||||
displayName, err := args.GetTwitchClient().GetDisplayNameForUser(context.Background(), strings.TrimLeft(username, "#"))
|
||||
if len(v) > 0 && (err != nil || displayName == "") {
|
||||
return v[0], nil //nolint:nilerr // Default value, no need to return error
|
||||
}
|
||||
|
||||
return displayName, err
|
||||
return displayName, errors.Wrap(err, "getting display name")
|
||||
}), plugins.TemplateFuncDocumentation{
|
||||
Description: "Returns the display name the specified user set for themselves",
|
||||
Syntax: "displayName <username> [fallback]",
|
||||
|
@ -38,7 +38,8 @@ func tplTwitchDisplayName(args plugins.RegistrationArguments) {
|
|||
|
||||
func tplTwitchIDForUsername(args plugins.RegistrationArguments) {
|
||||
args.RegisterTemplateFunction("idForUsername", plugins.GenericTemplateFunctionGetter(func(username string) (string, error) {
|
||||
return args.GetTwitchClient().GetIDForUsername(username)
|
||||
id, err := args.GetTwitchClient().GetIDForUsername(context.Background(), username)
|
||||
return id, errors.Wrap(err, "getting ID for username")
|
||||
}), plugins.TemplateFuncDocumentation{
|
||||
Description: "Returns the user-id for the given username",
|
||||
Syntax: "idForUsername <username>",
|
||||
|
@ -51,7 +52,7 @@ func tplTwitchIDForUsername(args plugins.RegistrationArguments) {
|
|||
|
||||
func tplTwitchProfileImage(args plugins.RegistrationArguments) {
|
||||
args.RegisterTemplateFunction("profileImage", plugins.GenericTemplateFunctionGetter(func(username string) (string, error) {
|
||||
user, err := args.GetTwitchClient().GetUserInformation(strings.TrimLeft(username, "#@"))
|
||||
user, err := args.GetTwitchClient().GetUserInformation(context.Background(), strings.TrimLeft(username, "#@"))
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "getting user info")
|
||||
}
|
||||
|
@ -69,7 +70,8 @@ func tplTwitchProfileImage(args plugins.RegistrationArguments) {
|
|||
|
||||
func tplTwitchUsernameForID(args plugins.RegistrationArguments) {
|
||||
args.RegisterTemplateFunction("usernameForID", plugins.GenericTemplateFunctionGetter(func(id string) (string, error) {
|
||||
return args.GetTwitchClient().GetUsernameForID(context.Background(), id)
|
||||
username, err := args.GetTwitchClient().GetUsernameForID(context.Background(), id)
|
||||
return username, errors.Wrap(err, "getting username for ID")
|
||||
}), plugins.TemplateFuncDocumentation{
|
||||
Description: "Returns the current login name of an user-id",
|
||||
Syntax: "usernameForID <user-id>",
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
|
||||
var userState = newTwitchUserStateStore()
|
||||
|
||||
// Register provides the plugins.RegisterFunc
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
if err := args.RegisterRawMessageHandler(rawMessageHandler); err != nil {
|
||||
return errors.Wrap(err, "registering raw message handler")
|
||||
|
|
101
irc.go
101
irc.go
|
@ -11,7 +11,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/irc.v4"
|
||||
|
||||
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
||||
|
@ -48,7 +48,7 @@ func registerRawMessageHandler(fn plugins.RawMessageHandlerFunc) error {
|
|||
type ircHandler struct {
|
||||
c *irc.Client
|
||||
conn *tls.Conn
|
||||
ctx context.Context
|
||||
ctx context.Context //nolint:containedctx
|
||||
ctxCancelFn func()
|
||||
user string
|
||||
}
|
||||
|
@ -56,7 +56,7 @@ type ircHandler struct {
|
|||
func newIRCHandler() (*ircHandler, error) {
|
||||
h := new(ircHandler)
|
||||
|
||||
_, username, err := twitchClient.GetAuthorizedUser()
|
||||
_, username, err := twitchClient.GetAuthorizedUser(context.Background())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "fetching username")
|
||||
}
|
||||
|
@ -68,7 +68,7 @@ func newIRCHandler() (*ircHandler, error) {
|
|||
return nil, errors.Wrap(err, "connect to IRC server")
|
||||
}
|
||||
|
||||
token, err := twitchClient.GetToken()
|
||||
token, err := twitchClient.GetToken(context.Background())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getting auth token")
|
||||
}
|
||||
|
@ -98,11 +98,13 @@ func (i ircHandler) Close() error {
|
|||
|
||||
func (i ircHandler) ExecuteJoins(channels []string) {
|
||||
for _, ch := range channels {
|
||||
//nolint:errcheck,gosec
|
||||
i.c.Write(fmt.Sprintf("JOIN #%s", strings.TrimLeft(ch, "#")))
|
||||
}
|
||||
}
|
||||
|
||||
func (i ircHandler) ExecutePart(channel string) {
|
||||
//nolint:errcheck,gosec
|
||||
i.c.Write(fmt.Sprintf("PART #%s", strings.TrimLeft(channel, "#")))
|
||||
}
|
||||
|
||||
|
@ -115,13 +117,14 @@ func (i ircHandler) Handle(c *irc.Client, m *irc.Message) {
|
|||
defer configLock.RUnlock()
|
||||
|
||||
if err := config.LogRawMessage(m); err != nil {
|
||||
log.WithError(err).Error("Unable to log raw message")
|
||||
logrus.WithError(err).Error("Unable to log raw message")
|
||||
}
|
||||
}(m)
|
||||
|
||||
switch m.Command {
|
||||
case "001":
|
||||
// 001 is a welcome event, so we join channels there
|
||||
//nolint:errcheck,gosec
|
||||
c.WriteMessage(&irc.Message{
|
||||
Command: "CAP",
|
||||
Params: []string{
|
||||
|
@ -173,8 +176,10 @@ func (i ircHandler) Handle(c *irc.Client, m *irc.Message) {
|
|||
case "RECONNECT":
|
||||
// RECONNECT (Twitch Commands)
|
||||
// In this case, reconnect and rejoin channels that were on the connection, as you would normally.
|
||||
log.Warn("We were asked to reconnect, closing connection")
|
||||
i.Close()
|
||||
logrus.Warn("We were asked to reconnect, closing connection")
|
||||
if err := i.Close(); err != nil {
|
||||
logrus.WithError(err).Error("closing IRC connection after reconnect")
|
||||
}
|
||||
|
||||
case "USERNOTICE":
|
||||
// USERNOTICE (Twitch Commands)
|
||||
|
@ -187,7 +192,7 @@ func (i ircHandler) Handle(c *irc.Client, m *irc.Message) {
|
|||
i.handleTwitchWhisper(m)
|
||||
|
||||
default:
|
||||
log.WithFields(log.Fields{
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"command": m.Command,
|
||||
"tags": m.Tags,
|
||||
"trailing": m.Trailing(),
|
||||
|
@ -196,13 +201,18 @@ func (i ircHandler) Handle(c *irc.Client, m *irc.Message) {
|
|||
}
|
||||
|
||||
if err := notifyRawMessageHandlers(m); err != nil {
|
||||
log.WithError(err).Error("Unable to notify raw message handlers")
|
||||
logrus.WithError(err).Error("Unable to notify raw message handlers")
|
||||
}
|
||||
}
|
||||
|
||||
func (i ircHandler) Run() error { return errors.Wrap(i.c.RunContext(i.ctx), "running IRC client") }
|
||||
|
||||
func (i ircHandler) SendMessage(m *irc.Message) error { return i.c.WriteMessage(m) }
|
||||
func (i ircHandler) SendMessage(m *irc.Message) (err error) {
|
||||
if err = i.c.WriteMessage(m); err != nil {
|
||||
return fmt.Errorf("writing message: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ircHandler) getChannel(m *irc.Message) string {
|
||||
if len(m.Params) > 0 {
|
||||
|
@ -230,19 +240,19 @@ func (i ircHandler) handleClearChat(m *irc.Message) {
|
|||
fields.Set("seconds", seconds)
|
||||
fields.Set("target_id", targetUserID)
|
||||
fields.Set("target_name", m.Trailing())
|
||||
log.WithFields(log.Fields(fields.Data())).Info("User was timed out")
|
||||
logrus.WithFields(logrus.Fields(fields.Data())).Info("User was timed out")
|
||||
|
||||
case hasTargetUserID:
|
||||
// User w/o Duration = Ban
|
||||
evt = eventTypeBan
|
||||
fields.Set("target_id", targetUserID)
|
||||
fields.Set("target_name", m.Trailing())
|
||||
log.WithFields(log.Fields(fields.Data())).Info("User was banned")
|
||||
logrus.WithFields(logrus.Fields(fields.Data())).Info("User was banned")
|
||||
|
||||
default:
|
||||
// No User = /clear
|
||||
evt = eventTypeClearChat
|
||||
log.WithFields(log.Fields(fields.Data())).Info("Chat was cleared")
|
||||
logrus.WithFields(logrus.Fields(fields.Data())).Info("Chat was cleared")
|
||||
}
|
||||
|
||||
go handleMessage(i.c, m, evt, fields)
|
||||
|
@ -254,7 +264,7 @@ func (i ircHandler) handleClearMessage(m *irc.Message) {
|
|||
"message_id": m.Tags["target-msg-id"],
|
||||
"target_name": m.Tags["login"],
|
||||
})
|
||||
log.WithFields(log.Fields(fields.Data())).
|
||||
logrus.WithFields(logrus.Fields(fields.Data())).
|
||||
WithField("message", m.Trailing()).
|
||||
Info("Message was deleted")
|
||||
go handleMessage(i.c, m, eventTypeDelete, fields)
|
||||
|
@ -297,14 +307,16 @@ func (i ircHandler) handlePermit(m *irc.Message) {
|
|||
"to": username,
|
||||
})
|
||||
|
||||
log.WithFields(fields.Data()).Debug("Added permit")
|
||||
timerService.AddPermit(m.Params[0], username)
|
||||
logrus.WithFields(fields.Data()).Debug("Added permit")
|
||||
if err := timerService.AddPermit(m.Params[0], username); err != nil {
|
||||
logrus.WithError(err).Error("adding permit")
|
||||
}
|
||||
|
||||
go handleMessage(i.c, m, eventTypePermit, fields)
|
||||
}
|
||||
|
||||
func (i ircHandler) handleTwitchNotice(m *irc.Message) {
|
||||
log.WithFields(log.Fields{
|
||||
logrus.WithFields(logrus.Fields{
|
||||
eventFieldChannel: i.getChannel(m),
|
||||
"tags": m.Tags,
|
||||
"trailing": m.Trailing(),
|
||||
|
@ -313,15 +325,15 @@ func (i ircHandler) handleTwitchNotice(m *irc.Message) {
|
|||
switch m.Tags["msg-id"] {
|
||||
case "":
|
||||
// Notices SHOULD have msg-id tags...
|
||||
log.WithField("msg", m).Warn("Received notice without msg-id")
|
||||
logrus.WithField("msg", m).Warn("Received notice without msg-id")
|
||||
|
||||
default:
|
||||
log.WithField("id", m.Tags["msg-id"]).Debug("unhandled notice received")
|
||||
logrus.WithField("id", m.Tags["msg-id"]).Debug("unhandled notice received")
|
||||
}
|
||||
}
|
||||
|
||||
func (i ircHandler) handleTwitchPrivmsg(m *irc.Message) {
|
||||
log.WithFields(log.Fields{
|
||||
logrus.WithFields(logrus.Fields{
|
||||
eventFieldChannel: i.getChannel(m),
|
||||
"name": m.Name,
|
||||
eventFieldUserName: m.User,
|
||||
|
@ -353,7 +365,7 @@ func (i ircHandler) handleTwitchPrivmsg(m *irc.Message) {
|
|||
eventFieldUserID: m.Tags["user-id"],
|
||||
})
|
||||
|
||||
log.WithFields(log.Fields(fields.Data())).Info("User spent bits in chat message")
|
||||
logrus.WithFields(logrus.Fields(fields.Data())).Info("User spent bits in chat message")
|
||||
|
||||
go handleMessage(i.c, m, eventTypeBits, fields)
|
||||
}
|
||||
|
@ -370,7 +382,7 @@ func (i ircHandler) handleTwitchPrivmsg(m *irc.Message) {
|
|||
"message": m.Trailing(),
|
||||
})
|
||||
|
||||
log.WithFields(log.Fields(fields.Data())).Info("User used hype-chat message")
|
||||
logrus.WithFields(logrus.Fields(fields.Data())).Info("User used hype-chat message")
|
||||
|
||||
go handleMessage(i.c, m, eventTypeHypeChat, fields)
|
||||
}
|
||||
|
@ -378,8 +390,9 @@ func (i ircHandler) handleTwitchPrivmsg(m *irc.Message) {
|
|||
go handleMessage(i.c, m, nil, nil)
|
||||
}
|
||||
|
||||
//nolint:funlen
|
||||
func (i ircHandler) handleTwitchUsernotice(m *irc.Message) {
|
||||
log.WithFields(log.Fields{
|
||||
logrus.WithFields(logrus.Fields{
|
||||
eventFieldChannel: i.getChannel(m),
|
||||
"tags": m.Tags,
|
||||
"trailing": m.Trailing(),
|
||||
|
@ -391,17 +404,23 @@ func (i ircHandler) handleTwitchUsernotice(m *irc.Message) {
|
|||
eventFieldUserID: m.Tags["user-id"],
|
||||
})
|
||||
|
||||
message := m.Trailing()
|
||||
if message == i.getChannel(m) {
|
||||
// If no message is given, Trailing yields the channel name
|
||||
message = ""
|
||||
}
|
||||
|
||||
switch m.Tags["msg-id"] {
|
||||
case "":
|
||||
// Notices SHOULD have msg-id tags...
|
||||
log.WithField("msg", m).Warn("Received usernotice without msg-id")
|
||||
logrus.WithField("msg", m).Warn("Received usernotice without msg-id")
|
||||
|
||||
case "announcement":
|
||||
evtData.SetFromData(map[string]any{
|
||||
"color": m.Tags["msg-param-color"],
|
||||
"message": m.Trailing(),
|
||||
})
|
||||
log.WithFields(log.Fields(evtData.Data())).Info("Announcement was made")
|
||||
logrus.WithFields(logrus.Fields(evtData.Data())).Info("Announcement was made")
|
||||
|
||||
go handleMessage(i.c, m, eventTypeAnnouncement, evtData)
|
||||
|
||||
|
@ -409,7 +428,7 @@ func (i ircHandler) handleTwitchUsernotice(m *irc.Message) {
|
|||
evtData.SetFromData(map[string]interface{}{
|
||||
"gifter": m.Tags["msg-param-sender-login"],
|
||||
})
|
||||
log.WithFields(log.Fields(evtData.Data())).Info("User upgraded to paid sub")
|
||||
logrus.WithFields(logrus.Fields(evtData.Data())).Info("User upgraded to paid sub")
|
||||
|
||||
go handleMessage(i.c, m, eventTypeGiftPaidUpgrade, evtData)
|
||||
|
||||
|
@ -418,17 +437,11 @@ func (i ircHandler) handleTwitchUsernotice(m *irc.Message) {
|
|||
"from": m.Tags["login"],
|
||||
"viewercount": i.tagToNumeric(m, "msg-param-viewerCount", 0),
|
||||
})
|
||||
log.WithFields(log.Fields(evtData.Data())).Info("Incoming raid")
|
||||
logrus.WithFields(logrus.Fields(evtData.Data())).Info("Incoming raid")
|
||||
|
||||
go handleMessage(i.c, m, eventTypeRaid, evtData)
|
||||
|
||||
case "resub":
|
||||
message := m.Trailing()
|
||||
if message == i.getChannel(m) {
|
||||
// If no message is given, Trailing yields the channel name
|
||||
message = ""
|
||||
}
|
||||
|
||||
evtData.SetFromData(map[string]interface{}{
|
||||
"from": m.Tags["login"],
|
||||
"message": message,
|
||||
|
@ -436,7 +449,7 @@ func (i ircHandler) handleTwitchUsernotice(m *irc.Message) {
|
|||
"subscribed_months": i.tagToNumeric(m, "msg-param-cumulative-months", 0),
|
||||
"plan": m.Tags["msg-param-sub-plan"],
|
||||
})
|
||||
log.WithFields(log.Fields(evtData.Data())).Info("User re-subscribed")
|
||||
logrus.WithFields(logrus.Fields(evtData.Data())).Info("User re-subscribed")
|
||||
|
||||
go handleMessage(i.c, m, eventTypeResub, evtData)
|
||||
|
||||
|
@ -446,7 +459,7 @@ func (i ircHandler) handleTwitchUsernotice(m *irc.Message) {
|
|||
"multi_month": i.tagToNumeric(m, "msg-param-multimonth-duration", 0),
|
||||
"plan": m.Tags["msg-param-sub-plan"],
|
||||
})
|
||||
log.WithFields(log.Fields(evtData.Data())).Info("User subscribed")
|
||||
logrus.WithFields(logrus.Fields(evtData.Data())).Info("User subscribed")
|
||||
|
||||
go handleMessage(i.c, m, eventTypeSub, evtData)
|
||||
|
||||
|
@ -461,7 +474,7 @@ func (i ircHandler) handleTwitchUsernotice(m *irc.Message) {
|
|||
"to": m.Tags["msg-param-recipient-user-name"],
|
||||
"total_gifted": i.tagToNumeric(m, "msg-param-sender-count", 0),
|
||||
})
|
||||
log.WithFields(log.Fields(evtData.Data())).Info("User gifted a sub")
|
||||
logrus.WithFields(logrus.Fields(evtData.Data())).Info("User gifted a sub")
|
||||
|
||||
go handleMessage(i.c, m, eventTypeSubgift, evtData)
|
||||
|
||||
|
@ -474,10 +487,24 @@ func (i ircHandler) handleTwitchUsernotice(m *irc.Message) {
|
|||
"plan": m.Tags["msg-param-sub-plan"],
|
||||
"total_gifted": i.tagToNumeric(m, "msg-param-sender-count", 0),
|
||||
})
|
||||
log.WithFields(log.Fields(evtData.Data())).Info("User gifted subs to the community")
|
||||
logrus.WithFields(logrus.Fields(evtData.Data())).Info("User gifted subs to the community")
|
||||
|
||||
go handleMessage(i.c, m, eventTypeSubmysterygift, evtData)
|
||||
|
||||
case "viewermilestone":
|
||||
switch m.Tags["msg-param-category"] {
|
||||
case "watch-streak":
|
||||
evtData.SetFromData(map[string]any{
|
||||
"message": message,
|
||||
"streak": i.tagToNumeric(m, "msg-param-value", 0),
|
||||
})
|
||||
logrus.WithFields(logrus.Fields(evtData.Data())).Info("User shared a watch-streak")
|
||||
|
||||
go handleMessage(i.c, m, eventTypeWatchStreak, evtData)
|
||||
|
||||
default:
|
||||
logrus.WithField("category", m.Tags["msg-param-category"]).Debug("found unhandled viewermilestone category")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue