1
0
Fork 0
mirror of https://github.com/Luzifer/twitch-bot.git synced 2025-03-15 03:07:43 +00:00

Compare commits

..

43 commits

Author SHA1 Message Date
99eecd1631
Release: Twitch-Bot v3.35.1 2024-12-12 11:18:57 +01:00
00320ba09c
Update node dependencies
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-12-12 11:11:58 +01:00
db2d80642a
Update Go dependencies
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-12-12 11:11:58 +01:00
3cfee5ccc9
[core] Fix: Reduce token requirements for category search
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-12-12 00:59:42 +01:00
9cac1686b8
CI: Configure git-changerelease
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-12-02 13:25:25 +01:00
f26ce9b0da
prepare release v3.35.0 2024-12-02 13:24:22 +01:00
dd80433cb0
[raffle] Fix: Raffle channel did not allow underscore in channel name
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-12-02 13:08:18 +01:00
0d76c58ede
[template] Add functions parseDuration, parseDurationToSeconds
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-11-25 00:40:53 +01:00
096657bcee
Improve CI and document Makefile
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-09-26 13:18:37 +02:00
ff475f286b
prepare release v3.34.0 2024-09-16 11:10:16 +02:00
06d7fcb019
Lint: Fix (theoretical) overflow issues
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-09-16 11:09:03 +02:00
710783aaf7
[core] Fix: StreamMarker contained wrong ID format
as of broken documentation

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-09-13 00:08:41 +02:00
19038dbc6e
[templating] Add currentVOD function
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-09-11 20:30:55 +02:00
740a71a173
[marker] Add marker info to actor result
in order to enable rules to access i.e. the position of the marker

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-09-11 14:23:42 +02:00
e0a8ce3684
[linkcheck] Fix: Replace static (deprecated) user-agent list
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-09-05 12:50:58 +02:00
5a8459cedc
[marker] Implement actor to create stream markers
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-09-04 00:06:55 +02:00
8819b4031a
prepare release v3.33.2 2024-08-27 17:08:56 +02:00
41535bc4df
Lint: Replace deprecated linter
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-08-27 14:52:36 +02:00
150daf8a80
[raffle] Lint: Ignore linter false-positive
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-08-27 14:49:21 +02:00
1d192ad796
[overlays] Fix KoFi donation currency in eventfeed
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-08-27 00:23:01 +02:00
b1ceb29bfb
prepare release v3.33.1 2024-08-14 16:23:51 +02:00
26a57c379d
[editor] Update dependencies
fixes CVE-2024-39338

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-08-14 16:13:47 +02:00
13bc753b7d
[core] Fix: Do not execute action after permission check
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-08-14 15:56:06 +02:00
e8d60e2733
[raffle] Fix: Send ID as string
in order to be able to transport big uint64 through JSON

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-08-11 00:59:46 +02:00
4964ed25cf
prepare release v3.33.0 2024-07-27 23:39:48 +02:00
014df155ae
[overlays] Fix: Transmit event-id as string
in order to compensate for i.e. CRDB very large IDs being truncated in
JSON transmit

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-07-26 14:43:05 +02:00
c4be936c63
[overlays] Add eventfeed as default-overlay
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-07-26 14:42:57 +02:00
b38ecc9d0b
[kofi] Fix: Use message as string
with pointer of string comparisons do not work properly and make
templating hard

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-07-03 12:27:54 +02:00
621d266391
[linkcheck] Add support for meta-redirects
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-06-10 14:17:49 +02:00
f1d4c1a283
prepare release v3.32.0 2024-06-09 13:52:51 +02:00
0355713f7c
CI: Disable SSL on mysql test container
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-06-09 13:25:06 +02:00
c63793be2d
Lint: Update linter config
and fix some newly appearing linter errors

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-06-09 13:01:32 +02:00
2a64caec09
[core] Fix: Include username and channel in ban errors
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-05-31 12:26:24 +02:00
8e8895d32e
[templating] Add streamIsLive function
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-05-26 15:50:10 +02:00
0a37873241
[core] Fix: Accept proper token declaration in Authorization header
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-05-24 13:59:29 +02:00
19a30d342a
prepare release v3.31.0 2024-05-13 18:33:34 +02:00
30305600e7
[spotify] Fix: Refresh-Token gets revoked when using two functions
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-05-13 18:26:38 +02:00
5dd6a5323c
[core] Add locking to prevent concurrent rule executions
refs 

ensures counter actions are not triggered concurrently by two persons

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-05-01 22:39:18 +02:00
a01ce9aa5f
prepare release v3.30.0 2024-04-26 19:49:45 +02:00
eb37a75da8
Update dependencies
remove addition of Sprig functions as korvike functions now are based on
the Sprig functions

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-04-26 19:34:35 +02:00
3cefd39960
CI: Update linter config
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-04-26 17:48:17 +02:00
ebf734be40
[templating] Add userExists function
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-04-18 00:26:14 +02:00
f56a7a3266
[eventsub] Suspicious user topics were moved from beta to v1
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-04-13 17:29:14 +02:00
78 changed files with 3461 additions and 791 deletions

19
.git_changerelease.yaml Normal file
View file

@ -0,0 +1,19 @@
---
# Template to format the commit message containing the changelog change
# which will be used to add the tag to.
release_commit_message: "Release: Twitch-Bot {{.Version}}"
# Commands to run before committing the changelog and adding the tag.
# Therefore these can add content to be included into the release-
# commit. These commands have access to the `TAG_VERSION` variable
# which contains the tag to be applied after the commit. If the
# command specified here is prefixed with a `-` sign, the exit status
# will not fail the release process. If it is not prefixed with a `-`
# a non-zero exit status will terminate the release process. The
# commands will be run from the repostory root, so sub-dirs MUST be
# specified. All commands are run as `bash -ec "..."` so you can use
# bash inside the commands.
pre_commit_commands: []
...

View file

@ -172,7 +172,7 @@ jobs:
run: git config --global --add safe.directory /__w/twitch-bot/twitch-bot
- name: Set up MySQL service
run: |
mariadb -h mysql -u root --password=root-pass <<EOF
mariadb --skip-ssl -h mysql -u root --password=root-pass <<EOF
CREATE DATABASE integration DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
GRANT ALL ON integration.* TO 'twitch-bot'@'%';
EOF

View file

@ -31,11 +31,11 @@ linters:
- 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]
- copyloopvar # copyloopvar is a linter detects places where loop variables are copied [fast: true, 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]
- gocognit # Computes and checks the cognitive complexity of functions [fast: true, auto-fix: false]
@ -46,12 +46,12 @@ linters:
- gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification [fast: true, auto-fix: true]
- gofumpt # Gofumpt checks whether code was gofumpt-ed. [fast: true, auto-fix: true]
- goimports # Goimports does everything that gofmt does. Additionally it checks unused imports [fast: true, auto-fix: true]
- gomnd # An analyzer to detect magic numbers. [fast: true, auto-fix: false]
- gosec # Inspects source code for security problems [fast: true, auto-fix: false]
- gosimple # Linter for Go source code that specializes in simplifying a code [fast: true, auto-fix: false]
- govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string [fast: true, auto-fix: false]
- 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]
- mnd # An analyzer to detect magic numbers. [fast: true, auto-fix: false]
- 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]
@ -77,9 +77,7 @@ linters-settings:
min-complexity: 15
gomnd:
settings:
mnd:
ignored-functions: 'strconv.(?:Format|Parse)\B+'
ignored-functions: 'strconv.(?:Format|Parse)\B+'
revive:
rules:

View file

@ -1,3 +1,81 @@
# 3.35.1 / 2024-12-12
* Bugfixes
* [core] Fix: Reduce token requirements for category search
* Update node dependencies
* Update Go dependencies
# 3.35.0 / 2024-12-02
* New Features
* [template] Add functions `parseDuration`, `parseDurationToSeconds`
* Bugfixes
* [raffle] Fix: Raffle channel did not allow underscore in channel name
# 3.34.0 / 2024-09-16
* New Features
* [marker] Implement actor to create stream markers
* [templating] Add `currentVOD` function
* Bugfixes
* [linkcheck] Fix: Replace static (deprecated) user-agent list
# 3.33.2 / 2024-08-27
* Bugfixes
* [overlays] Fix KoFi donation currency in eventfeed
* [raffle] Lint: Ignore linter false-positive
* [CI] Lint: Replace deprecated linter
# 3.33.1 / 2024-08-14
* Bugfixes
* [core] Fix: Do not execute action after permission check
* [editor] Update dependencies
* [raffle] Fix: Send ID as string
# 3.33.0 / 2024-07-27
* New Features
* [overlays] Add eventfeed as default-overlay
* Improvements
* [linkcheck] Add support for meta-redirects
* Bugfixes
* [kofi] Fix: Use message as string
* [overlays] Fix: Transmit event-id as string
# 3.32.0 / 2024-06-09
* New Features
* [templating] Add `streamIsLive` function
* Bugfixes
* [core] Fix: Accept proper token declaration in Authorization header
* [core] Fix: Include username and channel in ban errors
# 3.31.0 / 2024-05-13
* Improvements
* [core] Add locking to prevent concurrent rule executions
* Bugfixes
* [spotify] Fix: Refresh-Token gets revoked when using two functions
# 3.30.0 / 2024-04-26
* New Features
* [templating] Add `userExists` function
* Improvements
* [eventsub] Suspicious user topics were moved from beta to v1
* Bugfixes
* Update dependencies
# 3.29.2 / 2024-04-13
> [!IMPORTANT]

View file

@ -1,55 +1,80 @@
DOCS_BASE_URL:=/
HUGO_VERSION:=0.117.0
default: lint frontend_lint test
## Tool Binaries
GO_RUN := go run -modfile ./tools/go.mod
GO_TEST = $(GO_RUN) gotest.tools/gotestsum --format pkgname
GOLANCI_LINT = $(GO_RUN) github.com/golangci/golangci-lint/cmd/golangci-lint
build_prod: frontend_prod
##@ General
# The help target prints out all targets with their descriptions organized
# beneath their categories. The categories are represented by '##@' and the
# target descriptions by '##'. The awk commands is responsible for reading the
# entire set of makefiles included in this invocation, looking for lines of the
# file as xyz: ## something, and then pretty-format the target and help. Then,
# if there's a line with ##@ something, that gets pretty-printed as a category.
# More info on the usage of ANSI control characters for terminal formatting:
# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters
# More info on the awk command:
# http://linuxcommand.org/lc3_adv_awk.php
.PHONY: help
help: ## Display this help.
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
##@ Building
build_prod: frontend_prod ## Build release binary locally
go build \
-trimpath \
-mod=readonly \
-ldflags "-X main.version=$(shell git describe --tags --always || echo dev)"
lint:
golangci-lint run
publish: frontend_prod
publish: frontend_prod ## Run build tooling to produce all binaries
bash ./ci/build.sh
short_test:
go test -cover -test.short -v ./...
##@ Development
test:
go test -cover -v ./...
lint: ## Run Linter against code
$(GOLANCI_LINT) run ./...
# --- Editor frontend
short_test: ## Run tests not depending on network
$(GO_TEST) --hide-summary skipped -- ./... -cover -short
test: ## Run all tests
$(GO_TEST) --hide-summary skipped -- ./... -cover
##@ Editor frontend
frontend_prod: export NODE_ENV=production
frontend_prod: frontend
frontend_prod: frontend ## Build frontend in production mode
frontend: node_modules
frontend: node_modules ## Build frontend
node ci/build.mjs
frontend_lint: node_modules
frontend_lint: node_modules ## Lint frontend files
./node_modules/.bin/eslint \
--ext .js,.vue \
--fix \
src
node_modules:
node_modules: ## Install node modules
npm ci --include dev
# --- Tools
##@ Tooling
update_ua_list:
# User-Agents provided by https://www.useragents.me/
curl -sSf https://www.useragents.me/api | jq -r '.data[].ua' | grep -v 'Trident' >internal/linkcheck/user-agents.txt
update-chrome-major: ## Patch latest Chrome major version into linkcheck
sed -i -E \
's/chromeMajor = [0-9]+/chromeMajor = $(shell curl -sSf https://lv.luzifer.io/v1/catalog/google-chrome/stable/version | cut -d '.' -f 1)/' \
internal/linkcheck/useragent.go
gh-workflow:
gh-workflow: ## Regenerate CI workflow
bash ci/create-workflow.sh
# -- Vulnerability scanning --
##@ Vulnerability scanning
trivy:
trivy: ## Run Trivy against the code
trivy fs . \
--dependency-tree \
--exit-code 1 \
@ -58,23 +83,23 @@ trivy:
--quiet \
--scanners misconfig,license,secret,vuln \
--severity HIGH,CRITICAL \
--skip-dirs docs
--skip-dirs docs,tools
# -- Documentation Site --
##@ Documentation
docs: actor_docs eventclient_docs template_docs
docs: actor_docs eventclient_docs template_docs ## Generate all documentation
actor_docs:
actor_docs: ## Generate actor documentation
go run . --storage-conn-string $(shell mktemp).db actor-docs >docs/content/configuration/actors.md
template_docs:
template_docs: ## Generate template function documentation
go run . --storage-conn-string $(shell mktemp).db tpl-docs >docs/content/configuration/templating.md
eventclient_docs:
eventclient_docs: ## Generate eventclient documentation
echo -e "---\ntitle: EventClient\nweight: 10000\n---\n" >docs/content/overlays/eventclient.md
docker run --rm -i -v $(CURDIR):$(CURDIR) -w $(CURDIR) node:18-alpine sh -ec 'npx --yes jsdoc-to-markdown --files ./internal/apimodules/overlays/default/eventclient.js' >>docs/content/overlays/eventclient.md
render_docs: hugo_$(HUGO_VERSION)
render_docs: hugo_$(HUGO_VERSION) ## Render documentation site
./hugo_$(HUGO_VERSION) \
--baseURL "$(DOCS_BASE_URL)" \
--cleanDestinationDir \

View file

@ -1,6 +1,7 @@
package main
import (
"path"
"sync"
"github.com/pkg/errors"
@ -8,6 +9,7 @@ import (
"gopkg.in/irc.v4"
"github.com/Luzifer/go_helpers/v2/fieldcollection"
"github.com/Luzifer/twitch-bot/v3/internal/locker"
"github.com/Luzifer/twitch-bot/v3/plugins"
)
@ -79,6 +81,9 @@ func handleMessage(c *irc.Client, m *irc.Message, event *string, eventData *fiel
}
func handleMessageRuleExecution(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData *fieldcollection.FieldCollection) {
locker.LockByKey(path.Join("rule-execution", r.MatcherID()))
defer locker.UnlockByKey(path.Join("rule-execution", r.MatcherID()))
var (
ruleEventData = fieldcollection.NewFieldCollection()
preventCooldown bool

View file

@ -5,6 +5,7 @@ import (
"encoding/base64"
"fmt"
"net/http"
"strings"
"github.com/gofrs/uuid/v3"
"github.com/pkg/errors"
@ -51,7 +52,25 @@ func writeAuthMiddleware(h http.Handler, module string) http.Handler {
token = pass
case r.Header.Get("Authorization") != "":
token = r.Header.Get("Authorization")
var (
tokenType string
hadPrefix bool
)
tokenType, token, hadPrefix = strings.Cut(r.Header.Get("Authorization"), " ")
switch {
case !hadPrefix:
// Legacy: Accept `Authorization: tokenhere`
token = tokenType
case strings.EqualFold(tokenType, "token"):
// This is perfect: `Authorization: Token tokenhere`
default:
// That was unexpected: `Authorization: Bearer tokenhere` or similar
http.Error(w, "invalid token type", http.StatusForbidden)
return
}
default:
http.Error(w, "auth not successful", http.StatusForbidden)

View file

@ -43,7 +43,7 @@ steps:
- name: Set up MySQL service
run: |
mariadb -h mysql -u root --password=root-pass <<EOF
mariadb --skip-ssl -h mysql -u root --password=root-pass <<EOF
CREATE DATABASE integration DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
GRANT ALL ON integration.* TO 'twitch-bot'@'%';
EOF

82
cli.go
View file

@ -1,85 +1,7 @@
package main
import (
"fmt"
"os"
"sort"
"strings"
"sync"
"github.com/pkg/errors"
"github.com/Luzifer/go_helpers/v2/cli"
)
type (
cliRegistry struct {
cmds map[string]cliRegistryEntry
sync.Mutex
}
cliRegistryEntry struct {
Description string
Name string
Params []string
Run func([]string) error
}
)
var (
cli = newCLIRegistry()
errHelpCalled = errors.New("help called")
)
func newCLIRegistry() *cliRegistry {
return &cliRegistry{
cmds: make(map[string]cliRegistryEntry),
}
}
func (c *cliRegistry) Add(e cliRegistryEntry) {
c.Lock()
defer c.Unlock()
c.cmds[e.Name] = e
}
func (c *cliRegistry) Call(args []string) error {
c.Lock()
defer c.Unlock()
cmdEntry := c.cmds[args[0]]
if cmdEntry.Name != args[0] {
c.help()
return errHelpCalled
}
return cmdEntry.Run(args)
}
func (c *cliRegistry) help() {
// Called from Call, does not need lock
var (
maxCmdLen int
cmds []cliRegistryEntry
)
for name := range c.cmds {
entry := c.cmds[name]
if l := len(entry.CommandDisplay()); l > maxCmdLen {
maxCmdLen = l
}
cmds = append(cmds, entry)
}
sort.Slice(cmds, func(i, j int) bool { return cmds[i].Name < cmds[j].Name })
tpl := fmt.Sprintf(" %%-%ds %%s\n", maxCmdLen)
fmt.Fprintln(os.Stdout, "Supported sub-commands are:")
for _, cmd := range cmds {
fmt.Fprintf(os.Stdout, tpl, cmd.CommandDisplay(), cmd.Description)
}
}
func (c cliRegistryEntry) CommandDisplay() string {
return strings.Join(append([]string{c.Name}, c.Params...), " ")
}
var cliTool = cli.New()

View file

@ -4,11 +4,12 @@ import (
"bytes"
"os"
"github.com/Luzifer/go_helpers/v2/cli"
"github.com/pkg/errors"
)
func init() {
cli.Add(cliRegistryEntry{
cliTool.Add(cli.RegistryEntry{
Name: "actor-docs",
Description: "Generate markdown documentation for available actors",
Run: func([]string) error {

View file

@ -3,6 +3,7 @@ package main
import (
"os"
"github.com/Luzifer/go_helpers/v2/cli"
"github.com/gofrs/uuid/v3"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
@ -10,12 +11,12 @@ import (
)
func init() {
cli.Add(cliRegistryEntry{
cliTool.Add(cli.RegistryEntry{
Name: "api-token",
Description: "Generate an api-token to be entered into the config",
Params: []string{"<token-name>", "<scope>", "[...scope]"},
Run: func(args []string) error {
if len(args) < 3 { //nolint:gomnd // Just a count of parameters
if len(args) < 3 { //nolint:mnd // Just a count of parameters
return errors.New("Usage: twitch-bot api-token <token name> <scope> [...scope]")
}

View file

@ -3,6 +3,7 @@ package main
import (
"sync"
"github.com/Luzifer/go_helpers/v2/cli"
"github.com/Luzifer/twitch-bot/v3/pkg/database"
"github.com/Luzifer/twitch-bot/v3/plugins"
"github.com/pkg/errors"
@ -16,12 +17,12 @@ var (
)
func init() {
cli.Add(cliRegistryEntry{
cliTool.Add(cli.RegistryEntry{
Name: "copy-database",
Description: "Copies database contents to a new storage DSN i.e. for migrating to a new DBMS",
Params: []string{"<target storage-type>", "<target DSN>"},
Run: func(args []string) error {
if len(args) < 3 { //nolint:gomnd // Just a count of parameters
if len(args) < 3 { //nolint:mnd // Just a count of parameters
return errors.New("Usage: twitch-bot copy-database <target storage-type> <target DSN>")
}

View file

@ -1,12 +1,13 @@
package main
import (
"github.com/Luzifer/go_helpers/v2/cli"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)
func init() {
cli.Add(cliRegistryEntry{
cliTool.Add(cli.RegistryEntry{
Name: "reset-secrets",
Description: "Remove encrypted data to reset encryption passphrase",
Run: func([]string) error {

View file

@ -4,11 +4,12 @@ import (
"bytes"
"os"
"github.com/Luzifer/go_helpers/v2/cli"
"github.com/pkg/errors"
)
func init() {
cli.Add(cliRegistryEntry{
cliTool.Add(cli.RegistryEntry{
Name: "tpl-docs",
Description: "Generate markdown documentation for available template functions",
Run: func([]string) error {

View file

@ -1,9 +1,12 @@
package main
import "github.com/pkg/errors"
import (
"github.com/Luzifer/go_helpers/v2/cli"
"github.com/pkg/errors"
)
func init() {
cli.Add(cliRegistryEntry{
cliTool.Add(cli.RegistryEntry{
Name: "validate-config",
Description: "Try to load configuration file and report errors if any",
Run: func([]string) error {

View file

@ -211,7 +211,9 @@ func writeConfigToYAML(filename, authorName, authorEmail, summary string, obj *c
}
tmpFileName := tmpFile.Name()
fmt.Fprintf(tmpFile, "# Automatically updated by %s using Config-Editor frontend, last update: %s\n", authorName, time.Now().Format(time.RFC3339))
if _, err = fmt.Fprintf(tmpFile, "# Automatically updated by %s using Config-Editor frontend, last update: %s\n", authorName, time.Now().Format(time.RFC3339)); err != nil {
return fmt.Errorf("writing file header: %w", err)
}
if err = yaml.NewEncoder(tmpFile).Encode(obj); err != nil {
tmpFile.Close() //nolint:errcheck,gosec,revive

View file

@ -81,6 +81,7 @@ func configEditorHandleAutoMessageAdd(w http.ResponseWriter, r *http.Request) {
user, _, err := getAuthorizationFromRequest(r)
if err != nil {
http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError)
return
}
msg := &autoMessage{}
@ -106,6 +107,7 @@ func configEditorHandleAutoMessageDelete(w http.ResponseWriter, r *http.Request)
user, _, err := getAuthorizationFromRequest(r)
if err != nil {
http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError)
return
}
if err := patchConfig(cfg.Config, user, "", "Delete auto-message", func(c *configFile) error {
@ -142,6 +144,7 @@ func configEditorHandleAutoMessageUpdate(w http.ResponseWriter, r *http.Request)
user, _, err := getAuthorizationFromRequest(r)
if err != nil {
http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError)
return
}
msg := &autoMessage{}

View file

@ -172,6 +172,7 @@ func configEditorHandleGeneralDeleteAuthToken(w http.ResponseWriter, r *http.Req
user, _, err := getAuthorizationFromRequest(r)
if err != nil {
http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError)
return
}
if err := patchConfig(cfg.Config, user, "", "Delete auth-token", func(cfg *configFile) error {
@ -234,6 +235,7 @@ func configEditorHandleGeneralUpdate(w http.ResponseWriter, r *http.Request) {
user, _, err := getAuthorizationFromRequest(r)
if err != nil {
http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError)
return
}
var payload configEditorGeneralConfig

View file

@ -81,6 +81,7 @@ func configEditorRulesAdd(w http.ResponseWriter, r *http.Request) {
user, _, err := getAuthorizationFromRequest(r)
if err != nil {
http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError)
return
}
msg := &plugins.Rule{}
@ -119,6 +120,7 @@ func configEditorRulesDelete(w http.ResponseWriter, r *http.Request) {
user, _, err := getAuthorizationFromRequest(r)
if err != nil {
http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError)
return
}
if err := patchConfig(cfg.Config, user, "", "Delete rule", func(c *configFile) error {
@ -155,6 +157,7 @@ func configEditorRulesUpdate(w http.ResponseWriter, r *http.Request) {
user, _, err := getAuthorizationFromRequest(r)
if err != nil {
http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError)
return
}
msg := &plugins.Rule{}

View file

@ -9,7 +9,7 @@ import (
)
func updateConfigCron() string {
minute := rand.Intn(60) //nolint:gomnd,gosec // Only used to distribute load
minute := rand.Intn(60) //nolint:mnd,gosec // Only used to distribute load
return fmt.Sprintf("0 %d * * * *", minute)
}

View file

@ -84,6 +84,23 @@ Triggers the creation of a Clip from the given channel owned by the creator (sub
add_delay: false
```
## Create Marker
Creates a marker on the currently running stream of the given channel. The marker will be created on behalf of the channel owner and requires matching scope. (Subsequent actions can use variable `marker` to access marker details.)
```yaml
- type: marker
attributes:
# Channel to create the marker in, defaults to the channel of the event / message
# Optional: true
# Type: string (Supports Templating)
channel: ""
# Description of the marker to create (up to 140 chars)
# Optional: true
# Type: string (Supports Templating)
description: ""
```
## Custom Event
Create a custom event
@ -523,7 +540,7 @@ Send raw IRC message
## Send Whisper
Send a whisper (requires a verified bot!)
Send a whisper
```yaml
- type: whisper

View file

@ -165,6 +165,19 @@ Example:
* 1 6
```
### `currentVOD`
Returns the VOD of the currently running stream in the given channel (causes an error if no current stream / VOD is found)
Syntax: `currentVOD <username>`
Example:
```
# {{ currentVOD .channel }}
* https://www.twitch.tv/videos/123456789
```
### `displayName`
Returns the display name the specified user set for themselves
@ -379,6 +392,32 @@ Example:
< @user @user @user
```
### `parseDuration`
Parses a duration (i.e. 1h25m10s) into a time.Duration
Syntax: `parseDuration <duration>`
Example:
```
# {{ parseDuration "1h30s" }}
< 1h0m30s
```
### `parseDurationToSeconds`
Parses a duration (i.e. 1h25m10s) into a number of seconds
Syntax: `parseDurationToSeconds <duration>`
Example:
```
# {{ parseDurationToSeconds "1h25m10s" }}
< 5110
```
### `pow`
Returns float from calculation: `float1 ** float2`
@ -467,12 +506,12 @@ Example:
```
# Your int this hour: {{ printf "%.0f" (mulf (seededRandom (list "int" .username (now | date "2006-01-02 15") | join ":")) 100) }}%
< Your int this hour: 88%
< Your int this hour: 24%
```
### `spotifyCurrentPlaying`
Retrieves the current playing track for the given channel
Retrieves the current playing track for the given channel (returns an empty string when nothing is playing)
Syntax: `spotifyCurrentPlaying <channel>`
@ -487,7 +526,7 @@ Example:
### `spotifyLink`
Retrieves the link for the playing track for the given channel
Retrieves the link for the playing track for the given channel (returns an empty string when nothing is playing)
Syntax: `spotifyLink <channel>`
@ -500,6 +539,19 @@ Example:
* https://open.spotify.com/track/3HCzXf0lNpekSqsGBcGrCd
```
### `streamIsLive`
Check whether a given channel is currently live
Syntax: `streamIsLive <username>`
Example:
```
# {{ streamIsLive "luziferus" }}
* true
```
### `streamUptime`
Returns the duration the stream is online (causes an error if no current stream is found)
@ -567,6 +619,19 @@ Example:
* Weather for Hamburg, DE: Few clouds with a temperature of 22 C (71.6 F). [...]
```
### `userExists`
Checks whether the given user exists
Syntax: `userExists <username>`
Example:
```
# {{ userExists "luziferus" }}
* true
```
### `usernameForID`
Returns the current login name of an user-id

View file

@ -116,7 +116,7 @@ SocketMessage received for every event and passed to the new `(eventObj) => { ..
| Name | Type | Description |
| --- | --- | --- |
| [event_id] | <code>Number</code> | UID of the event used to re-trigger an event |
| [event_id] | <code>String</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 |

View file

@ -7,21 +7,15 @@ import (
"text/template"
"time"
"github.com/Masterminds/sprig/v3"
"github.com/sirupsen/logrus"
"gopkg.in/irc.v4"
"github.com/Luzifer/go_helpers/v2/fieldcollection"
"github.com/Luzifer/go_helpers/v2/str"
korvike "github.com/Luzifer/korvike/functions"
"github.com/Luzifer/twitch-bot/v3/plugins"
)
var (
korvikeBlacklist = []string{"now"}
sprigBlacklist = []string{"env"}
tplFuncs = newTemplateFuncProvider()
)
var tplFuncs = newTemplateFuncProvider()
type templateFuncProvider struct {
docs []plugins.TemplateFuncDocumentation
@ -44,16 +38,6 @@ func (t *templateFuncProvider) GetFuncMap(m *irc.Message, r *plugins.Rule, field
out := make(template.FuncMap)
for n, fn := range sprig.TxtFuncMap() {
if str.StringInSlice(n, sprigBlacklist) {
continue
}
if out[n] != nil {
panic(fmt.Sprintf("duplicate function: %s (add in sprig)", n))
}
out[n] = fn
}
for n, fg := range t.funcs {
if out[n] != nil {
panic(fmt.Sprintf("duplicate function: %s (add in registration)", n))
@ -93,9 +77,6 @@ func (t *templateFuncProvider) Register(name string, fg plugins.TemplateFuncGett
func init() {
// Register Korvike functions
for n, f := range korvike.GetFunctionMap() {
if str.StringInSlice(n, korvikeBlacklist) {
continue
}
tplFuncs.Register(n, plugins.GenericTemplateFunctionGetter(f))
}

100
go.mod
View file

@ -1,77 +1,76 @@
module github.com/Luzifer/twitch-bot/v3
go 1.21
go 1.22.0
toolchain go1.23.4
require (
github.com/Luzifer/go-openssl/v4 v4.2.2
github.com/Luzifer/go_helpers/v2 v2.24.0
github.com/Luzifer/korvike/functions v0.11.0
github.com/Luzifer/rconfig/v2 v2.5.0
github.com/Masterminds/sprig/v3 v3.2.3
github.com/getsentry/sentry-go v0.27.0
github.com/Luzifer/go_helpers/v2 v2.25.0
github.com/Luzifer/korvike/functions v1.0.1
github.com/Luzifer/rconfig/v2 v2.5.2
github.com/getsentry/sentry-go v0.30.0
github.com/glebarez/sqlite v1.11.0
github.com/go-git/go-git/v5 v5.12.0
github.com/go-sql-driver/mysql v1.8.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.15
github.com/gorilla/websocket v1.5.3
github.com/itchyny/gojq v0.12.17
github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/orandin/sentrus v1.0.0
github.com/pkg/errors v0.9.1
github.com/robfig/cron/v3 v3.0.1
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.9.0
github.com/stretchr/testify v1.10.0
github.com/wzshiming/openapi v0.0.0-20200703171632-c7220b3c9cfb
golang.org/x/crypto v0.21.0
golang.org/x/net v0.22.0
golang.org/x/oauth2 v0.18.0
golang.org/x/crypto v0.31.0
golang.org/x/net v0.32.0
golang.org/x/oauth2 v0.24.0
gopkg.in/irc.v4 v4.0.0
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/mysql v1.5.6
gorm.io/driver/postgres v1.5.7
gorm.io/gorm v1.25.9
gorm.io/driver/mysql v1.5.7
gorm.io/driver/postgres v1.5.11
gorm.io/gorm v1.25.12
)
require (
dario.cat/mergo v1.0.0 // indirect
dario.cat/mergo v1.0.1 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
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 v1.0.0 // indirect
github.com/cenkalti/backoff/v3 v3.2.2 // 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/Masterminds/semver/v3 v3.3.1 // indirect
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.1.3 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cloudflare/circl v1.5.0 // indirect
github.com/cyphar/filepath-securejoin v0.3.5 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emirpasic/gods v1.18.1 // 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.3 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/go-git/go-billy/v5 v5.6.0 // indirect
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // 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
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8 // indirect
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.12.2 // 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/hashicorp/go-sockaddr v1.0.7 // indirect
github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
github.com/hashicorp/vault/api v1.15.0 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/itchyny/timefmt-go v0.1.6 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
github.com/jackc/pgx/v5 v5.5.5 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.1 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
@ -83,28 +82,25 @@ require (
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/skeema/knownhosts v1.2.2 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/skeema/knownhosts v1.3.0 // indirect
github.com/spf13/cast v1.7.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/mod v0.16.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.19.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.33.0 // indirect
golang.org/x/exp v0.0.0-20241210194714-1829a127f884 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.8.0 // indirect
gopkg.in/validator.v2 v2.0.1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
modernc.org/libc v1.49.0 // indirect
modernc.org/libc v1.61.4 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/sqlite v1.29.5 // indirect
modernc.org/sqlite v1.34.2 // indirect
)

383
go.sum
View file

@ -1,63 +1,52 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
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.24.0 h1:abACOhsn6a6c6X22jq42mZM1wuOM0Ihfa6yzssrjrOg=
github.com/Luzifer/go_helpers/v2 v2.24.0/go.mod h1:KSVUdAJAav5cWGyB5oKGxmC27HrKULVTOxwPS/Kr+pc=
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.5.0 h1:zx5lfQbNX3za4VegID97IeY+M+BmfgHxWJTYA94sxok=
github.com/Luzifer/rconfig/v2 v2.5.0/go.mod h1:eGWUPQeCPv/Pr/p0hjmwFgI20uqvwi/Szen69hUzGzU=
github.com/Luzifer/go_helpers/v2 v2.25.0 h1:k1J4gd1+BfuokTDoWgcgib9P5mdadjzKEgbtKSVe46k=
github.com/Luzifer/go_helpers/v2 v2.25.0/go.mod h1:KSVUdAJAav5cWGyB5oKGxmC27HrKULVTOxwPS/Kr+pc=
github.com/Luzifer/korvike/functions v1.0.1 h1:9O9PQL7O8J3nBwR4XLyx4COC430QbnvueM+itA2HEto=
github.com/Luzifer/korvike/functions v1.0.1/go.mod h1:8U01t/IM4wmZcaEf7u/szQrbLvBGMPOTLzJI/l7baWI=
github.com/Luzifer/rconfig/v2 v2.5.2 h1:4Bfp8mTrCCK/xghUmUbh/qtKiLZA6RC0tHTgqkNw1m4=
github.com/Luzifer/rconfig/v2 v2.5.2/go.mod h1:HnqUWg+NQh60/neUqfMDDDo5d1v8UPuhwKR1HqM4VWQ=
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=
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
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 v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk=
github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
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=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M=
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.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/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys=
github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cyphar/filepath-securejoin v0.3.5 h1:L81NHjquoQmcPgXcttUS9qTSR/+bXry6pbSINQGpjj4=
github.com/cyphar/filepath-securejoin v0.3.5/go.mod h1:edhVd3c6OXKjUmSrVa/tGJRS9joFTxlslFCAyaxigkE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
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.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
github.com/getsentry/sentry-go v0.30.0 h1:lWUwDnY7sKHaVIoZ9wYqRHJ5iEmoc0pqcRqFkosKzBo=
github.com/getsentry/sentry-go v0.30.0/go.mod h1:WU9B9/1/sHDqeV8T+3VwwbjeR5MSXs/6aqG3mqZrezA=
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.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
@ -68,111 +57,72 @@ github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxI
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8=
github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM=
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.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY=
github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw=
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid/v3 v3.1.2 h1:V3IBv1oU82x6YIr5txe3azVHgmOKYdyKQTowm9moBlY=
github.com/gofrs/uuid/v3 v3.1.2/go.mod h1:xPwMqoocQ1L5G6pXX5BcE7N5jlzn2o19oqAKxwZW/kI=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
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=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI=
github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-hclog v1.4.0 h1:ctuWFGrhFha8BnnzxqeRGidlEcQkDyL5u8J8t5eA11I=
github.com/hashicorp/go-hclog v1.4.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY=
github.com/hashicorp/go-retryablehttp v0.5.4/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M=
github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8 h1:iBt4Ew4XEGLfh6/bPk4rSYmuZJGizr6/x/AEizP0CQc=
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8/go.mod h1:aiJI+PIApBRQG7FZTEBx5GiiX+HbOHilUdNxUZi4eV0=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
github.com/hashicorp/go-sockaddr v1.0.6 h1:RSG8rKU28VTUTvEKghe5gIhIQpv8evvNpnDEyqO4u9I=
github.com/hashicorp/go-sockaddr v1.0.6/go.mod h1:uoUUmtwU7n9Dv3O4SNLeFvg0SxQ3lyjsj6+CCykpaxI=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
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.12.2 h1:7YkCTE5Ni90TcmYHDBExdt4WGJxhpzaHqR6uGbQb/rE=
github.com/hashicorp/vault/api v1.12.2/go.mod h1:LSGf1NGT1BnvFFnKVtnvcaLBM2Lz+gJdpL6HUYed8KE=
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=
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
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.15 h1:WC1Nxbx4Ifw5U2oQWACYz32JK8G9qxNtHzrvW4KEcqI=
github.com/itchyny/gojq v0.12.15/go.mod h1:uWAHCbCIla1jiNxmeT5/B5mOjSdfkCq6p8vxWg+BM10=
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/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw=
github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw=
github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I=
github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM=
github.com/hashicorp/vault/api v1.15.0 h1:O24FYQCWwhwKnF7CuSqP30S51rTV7vz1iACXE/pj5DA=
github.com/hashicorp/vault/api v1.15.0/go.mod h1:+5YTO09JGn0u+b6ySD/LLVf8WkJCPLAL2Vkmrn2+CM8=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/itchyny/gojq v0.12.17 h1:8av8eGduDb5+rvEdaOO+zQUjA04MS0m3Ps8HiD+fceg=
github.com/itchyny/gojq v0.12.17/go.mod h1:WBrEMkgAfAGO1LUcGOckBl5O726KPp+OlkKug0I/FEY=
github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/myJ5Q=
github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg=
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-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.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
github.com/jackc/pgx/v5 v5.5.5/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/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs=
github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
@ -188,77 +138,56 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/orandin/sentrus v1.0.0 h1:rMZKTUdwuhIaC7C6VbvhQPQeO9hBpliODrj7o/NmipM=
github.com/orandin/sentrus v1.0.0/go.mod h1:Mqa1Dcat0IcuD/XPMXUolzuZ74NWptnnX8eRq3gLaSU=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/russross/blackfriday/v2 v2.1.0-pre.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A=
github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY=
github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M=
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.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=
@ -266,146 +195,56 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/wzshiming/openapi v0.0.0-20200703171632-c7220b3c9cfb h1:G0Rrif8QdbAz7Xy53H4Xumy6TuyKHom8pu8z/jdLwwM=
github.com/wzshiming/openapi v0.0.0-20200703171632-c7220b3c9cfb/go.mod h1:398xiAftMV/w8frjipnUzjr/WQ+E2fnGRv9yXobxyyk=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
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.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20241210194714-1829a127f884 h1:Y/Mj/94zIQQGHVSv1tTtQBDaQaJe62U9bkDZKKyhPCU=
golang.org/x/exp v0.0.0-20241210194714-1829a127f884/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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.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=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
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=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
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.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
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=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/irc.v4 v4.0.0 h1:5jsLkU2Tg+R2nGNqmkGCrciasyi4kNkDXhyZD+C31yY=
gopkg.in/irc.v4 v4.0.0/go.mod h1:BfjDz9MmuWW6OZY7iq4naOhudO8+QQCdO4Ko18jcsRE=
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/validator.v2 v2.0.1 h1:xF0KWyGWXm/LM2G1TrEjqOu4pa6coO9AlWSf3msVfDY=
gopkg.in/validator.v2 v2.0.1/go.mod h1:lIUZBlB3Im4s/eYp39Ry/wkR02yOPhZ9IwIRBjuPuG8=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
@ -417,25 +256,23 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM=
gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA=
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.9 h1:wct0gxZIELDk8+ZqF/MVnHLkA1rvYlBWUMv2EdsK1g8=
gorm.io/gorm v1.25.9/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/cc/v4 v4.19.5 h1:QlsZyQ1zf78DGeqnQ9ILi9hXyMdoC5e1qoGNUyBjHQw=
modernc.org/cc/v4 v4.19.5/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v4 v4.13.1 h1:qBttaSxEHNze36VBivw1/vkHuyjMDN3RY5wQX+p1Oxg=
modernc.org/ccgo/v4 v4.13.1/go.mod h1:Td6RI9W9G2ZpKHaJ7UeGEiB2aIpoDqLBnm4wtkbJTbQ=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
modernc.org/cc/v4 v4.23.1 h1:WqJoPL3x4cUufQVHkXpXX7ThFJ1C4ik80i2eXEXbhD8=
modernc.org/cc/v4 v4.23.1/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v4 v4.23.1 h1:N49a7JiWGWV7lkPE4yYcvjkBGZQi93/JabRYjdWmJXc=
modernc.org/ccgo/v4 v4.23.1/go.mod h1:JoIUegEIfutvoWV/BBfDFpPpfR2nc3U0jKucGcbmwDU=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/libc v1.49.0 h1:/kkNBuCXvlTbOGwrQdgR67eK1Y9+kR+fhdBd89C64VM=
modernc.org/libc v1.49.0/go.mod h1:DNz0lgQgT6FPIPm8rHtjFj0FL5/YOr/NYFXWYBcSxMw=
modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M=
modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/libc v1.61.4 h1:wVyqEx6tlltte9lPTjq0kDAdtdM9c4JH8rU6M1ZVawA=
modernc.org/libc v1.61.4/go.mod h1:VfXVuM/Shh5XsMNrh3C6OkfL78G3loa4ZC/Ljv9k7xc=
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.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
@ -444,8 +281,8 @@ modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
modernc.org/sqlite v1.29.5 h1:8l/SQKAjDtZFo9lkJLdk8g9JEOeYRG4/ghStDCCTiTE=
modernc.org/sqlite v1.29.5/go.mod h1:S02dvcmm7TnTRvGhv8IGYyLnIt7AS2KPaB1F/71p75U=
modernc.org/sqlite v1.34.2 h1:J9n76TPsfYYkFkZ9Uy1QphILYifiVEwwOT7yP5b++2Y=
modernc.org/sqlite v1.34.2/go.mod h1:dnR723UrTtjKpoHCAMN0Q/gZ9MT4r+iRvIBb9umWFkU=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=

View file

@ -336,7 +336,7 @@ func routeActorCounterGetValue(w http.ResponseWriter, r *http.Request) {
}
w.Header().Set("Content-Type", "text-plain")
fmt.Fprintf(w, template, cv)
http.Error(w, fmt.Sprintf(template, cv), http.StatusOK)
}
func routeActorCounterSetValue(w http.ResponseWriter, r *http.Request) {

View file

@ -0,0 +1,114 @@
// Package marker contains an actor to create markers on the current
// running stream
package marker
import (
"context"
"fmt"
"strings"
"github.com/Luzifer/go_helpers/v2/fieldcollection"
"github.com/Luzifer/twitch-bot/v3/internal/helpers"
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
"github.com/Luzifer/twitch-bot/v3/plugins"
"gopkg.in/irc.v4"
)
const actorName = "marker"
var (
formatMessage plugins.MsgFormatter
hasPerm plugins.ChannelPermissionCheckFunc
tcGetter func(string) (*twitch.Client, error)
)
// Register provides the plugins.RegisterFunc
func Register(args plugins.RegistrationArguments) error {
formatMessage = args.FormatMessage
hasPerm = args.HasPermissionForChannel
tcGetter = args.GetTwitchClientForChannel
args.RegisterActor(actorName, func() plugins.Actor { return &actor{} })
args.RegisterActorDocumentation(plugins.ActionDocumentation{
Description: "Creates a marker on the currently running stream of the given channel. The marker will be created on behalf of the channel owner and requires matching scope. (Subsequent actions can use variable `marker` to access marker details.)",
Name: "Create Marker",
Type: actorName,
Fields: []plugins.ActionDocumentationField{
{
Description: "Channel to create the marker in, defaults to the channel of the event / message",
Key: "channel",
Name: "Channel",
Optional: true,
SupportTemplate: true,
Type: plugins.ActionDocumentationFieldTypeString,
},
{
Description: "Description of the marker to create (up to 140 chars)",
Key: "description",
Name: "Description",
Optional: true,
SupportTemplate: true,
Type: plugins.ActionDocumentationFieldTypeString,
},
},
})
return nil
}
type actor struct{}
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *fieldcollection.FieldCollection, attrs *fieldcollection.FieldCollection) (preventCooldown bool, err error) {
channel := plugins.DeriveChannel(m, eventData)
if channel, err = formatMessage(attrs.MustString("channel", &channel), m, r, eventData); err != nil {
return false, fmt.Errorf("parsing channel: %w", err)
}
var description string
if description, err = formatMessage(attrs.MustString("description", &description), m, r, eventData); err != nil {
return false, fmt.Errorf("parsing description: %w", err)
}
channel = strings.TrimLeft(channel, "#")
canCreate, err := hasPerm(channel, twitch.ScopeChannelManageBroadcast)
if err != nil {
return false, fmt.Errorf("checking for required permission: %w", err)
}
if !canCreate {
return false, fmt.Errorf("creator has not given %s permission", twitch.ScopeChannelManageBroadcast)
}
tc, err := tcGetter(channel)
if err != nil {
return false, fmt.Errorf("getting Twitch client for %q: %w", channel, err)
}
var marker twitch.StreamMarkerInfo
if marker, err = tc.CreateStreamMarker(context.TODO(), description); err != nil {
return false, fmt.Errorf("creating marker: %w", err)
}
eventData.Set("marker", marker)
return false, nil
}
func (actor) IsAsync() bool { return false }
func (actor) Name() string { return actorName }
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *fieldcollection.FieldCollection) (err error) {
if err = attrs.ValidateSchema(
fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "channel", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "description", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}),
fieldcollection.MustHaveNoUnknowFields,
helpers.SchemaValidateTemplateField(tplValidator, "channel", "description"),
); err != nil {
return fmt.Errorf("validating attributes: %w", err)
}
return nil
}

View file

@ -0,0 +1,101 @@
package spotify
import (
"context"
"fmt"
"net/http"
"strings"
"time"
"github.com/Luzifer/twitch-bot/v3/internal/locker"
"github.com/sirupsen/logrus"
"golang.org/x/oauth2"
)
const expiryGrace = 10 * time.Second
func getAuthorizedClient(channel, redirectURL string) (client *http.Client, err error) {
// In templating functions are called multiple times at once which
// with Spotify replacing the refresh-token on each renew would kill
// the stored token when multiple spotify functions are called at
// once. Therefore we do have this method locking itself until it
// has successfully made one request to the users profile and therefore
// renewed the token. The next request then will use the token the
// previous request renewed.
locker.LockByKey(strings.Join([]string{"spotify", "api-access", channel}, ":"))
defer locker.UnlockByKey(strings.Join([]string{"spotify", "api-access", channel}, ":"))
conf, err := oauthConfig(channel, redirectURL)
if err != nil {
return nil, fmt.Errorf("getting oauth config: %w", err)
}
var token *oauth2.Token
if err = db.ReadEncryptedCoreMeta(strings.Join([]string{"spotify-auth", channel}, ":"), &token); err != nil {
return nil, fmt.Errorf("loading oauth token: %w", err)
}
ts := conf.TokenSource(context.Background(), token)
if token.Expiry.After(time.Now().Add(expiryGrace)) {
// Token is still valid long enough, we spare the resources to do
// the profile fetch and directly return the client with the token
// as the scenario described here does not apply.
return oauth2.NewClient(context.Background(), ts), nil
}
logrus.WithField("channel", channel).Debug("refreshing spotify token")
ctx, cancel := context.WithTimeout(context.Background(), spotifyRequestTimeout)
defer cancel()
// We do a request to /me once to refresh the token if needed
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.spotify.com/v1/me", nil)
if err != nil {
return nil, fmt.Errorf("creating currently-playing request: %w", err)
}
oauthClient := oauth2.NewClient(context.Background(), ts)
resp, err := oauthClient.Do(req)
if err != nil {
return nil, fmt.Errorf("executing request: %w", err)
}
defer func() {
if err := resp.Body.Close(); err != nil {
logrus.WithError(err).Error("closing Spotify response body (leaked fd)")
}
}()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("requesting user profile: %w", err)
}
updToken, err := ts.Token()
if err != nil {
return nil, fmt.Errorf("getting updated token: %w", err)
}
if err := db.StoreEncryptedCoreMeta(strings.Join([]string{"spotify-auth", channel}, ":"), updToken); err != nil {
logrus.WithError(err).Error("storing back Spotify auth token")
}
return oauthClient, nil
}
func oauthConfig(channel, redirectURL string) (conf *oauth2.Config, err error) {
clientID, err := getModuleConfig(actorName, channel).String("clientId")
if err != nil {
return nil, fmt.Errorf("getting clientId for channel: %w", err)
}
return &oauth2.Config{
ClientID: clientID,
Endpoint: oauth2.Endpoint{
AuthURL: "https://accounts.spotify.com/authorize",
TokenURL: "https://accounts.spotify.com/api/token",
},
RedirectURL: redirectURL,
Scopes: []string{"user-read-currently-playing"},
}, nil
}

View file

@ -3,34 +3,25 @@ package spotify
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"github.com/sirupsen/logrus"
"golang.org/x/oauth2"
)
var errNotPlaying = errors.New("nothing playing")
func getCurrentTrackForChannel(channel string) (track currentPlayingTrackResponse, err error) {
channel = strings.TrimLeft(channel, "#")
conf, err := oauthConfig(channel, "")
client, err := getAuthorizedClient(channel, "")
if err != nil {
return track, fmt.Errorf("getting oauth config: %w", err)
return track, fmt.Errorf("retrieving authorized Spotify client: %w", err)
}
var token *oauth2.Token
if err = db.ReadEncryptedCoreMeta(strings.Join([]string{"spotify-auth", channel}, ":"), &token); err != nil {
return track, fmt.Errorf("loading oauth token: %w", err)
}
defer func() {
if err := db.StoreEncryptedCoreMeta(strings.Join([]string{"spotify-auth", channel}, ":"), token); err != nil {
logrus.WithError(err).Error("storing back Spotify auth token")
}
}()
ctx, cancel := context.WithTimeout(context.Background(), spotifyRequestTimeout)
defer cancel()
@ -39,7 +30,7 @@ func getCurrentTrackForChannel(channel string) (track currentPlayingTrackRespons
return track, fmt.Errorf("creating currently-playing request: %w", err)
}
resp, err := conf.Client(context.Background(), token).Do(req)
resp, err := client.Do(req)
if err != nil {
return track, fmt.Errorf("executing request: %w", err)
}
@ -58,6 +49,10 @@ func getCurrentTrackForChannel(channel string) (track currentPlayingTrackRespons
case http.StatusOK:
// This is perfect, continue below
case http.StatusNoContent:
// User is not playing anything
return track, errNotPlaying
case http.StatusUnauthorized:
// The token is FUBAR
return track, fmt.Errorf("token expired (HTTP 401 - unauthorized)")
@ -85,6 +80,10 @@ func getCurrentTrackForChannel(channel string) (track currentPlayingTrackRespons
func getCurrentArtistTitleForChannel(channel string) (artistTitle string, err error) {
track, err := getCurrentTrackForChannel(channel)
if err != nil {
if errors.Is(err, errNotPlaying) {
return "", nil
}
return "", fmt.Errorf("getting track info: %w", err)
}
@ -102,6 +101,10 @@ func getCurrentArtistTitleForChannel(channel string) (artistTitle string, err er
func getCurrentLinkForChannel(channel string) (link string, err error) {
track, err := getCurrentTrackForChannel(channel)
if err != nil {
if errors.Is(err, errNotPlaying) {
return "", nil
}
return "", fmt.Errorf("getting track info: %w", err)
}

View file

@ -68,22 +68,5 @@ func handleStartAuth(w http.ResponseWriter, r *http.Request) {
return
}
fmt.Fprintln(w, "Spotify is now authorized for this channel, you can close this page")
}
func oauthConfig(channel, redirectURL string) (conf *oauth2.Config, err error) {
clientID, err := getModuleConfig(actorName, channel).String("clientId")
if err != nil {
return nil, fmt.Errorf("getting clientId for channel: %w", err)
}
return &oauth2.Config{
ClientID: clientID,
Endpoint: oauth2.Endpoint{
AuthURL: "https://accounts.spotify.com/authorize",
TokenURL: "https://accounts.spotify.com/api/token",
},
RedirectURL: redirectURL,
Scopes: []string{"user-read-currently-playing"},
}, nil
http.Error(w, "Spotify is now authorized for this channel, you can close this page", http.StatusOK)
}

View file

@ -35,7 +35,7 @@ func Register(args plugins.RegistrationArguments) (err error) {
plugins.GenericTemplateFunctionGetter(getCurrentArtistTitleForChannel),
plugins.TemplateFuncDocumentation{
Name: "spotifyCurrentPlaying",
Description: "Retrieves the current playing track for the given channel",
Description: "Retrieves the current playing track for the given channel (returns an empty string when nothing is playing)",
Syntax: "spotifyCurrentPlaying <channel>",
Example: &plugins.TemplateFuncDocumentationExample{
MatchMessage: "^!spotify",
@ -51,7 +51,7 @@ func Register(args plugins.RegistrationArguments) (err error) {
plugins.GenericTemplateFunctionGetter(getCurrentLinkForChannel),
plugins.TemplateFuncDocumentation{
Name: "spotifyLink",
Description: "Retrieves the link for the playing track for the given channel",
Description: "Retrieves the link for the playing track for the given channel (returns an empty string when nothing is playing)",
Syntax: "spotifyLink <channel>",
Example: &plugins.TemplateFuncDocumentationExample{
MatchMessage: "^!spotifylink",

View file

@ -195,7 +195,7 @@ func routeActorSetVarGetValue(w http.ResponseWriter, r *http.Request) {
}
w.Header().Set("Content-Type", "text-plain")
fmt.Fprint(w, vc)
http.Error(w, vc, http.StatusOK)
}
func routeActorSetVarSetValue(w http.ResponseWriter, r *http.Request) {

View file

@ -188,7 +188,7 @@ func handleModVIP(m *irc.Message, modFn func(tc *twitch.Client, channel, user st
channel := strings.TrimLeft(plugins.DeriveChannel(m, nil), "#")
parts := strings.Split(m.Trailing(), " ")
if len(parts) != 2 { //nolint:gomnd // Just a count, makes no sense as a constant
if len(parts) != 2 { //nolint:mnd // Just a count, makes no sense as a constant
return errors.Errorf("wrong command usage, must consist of 2 words")
}

View file

@ -100,8 +100,8 @@ func handleKoFiPost(w http.ResponseWriter, r *http.Request) {
fields.Set("isSubscription", payload.IsSubscriptionPayment)
fields.Set("isFirstSubPayment", payload.IsFirstSubscriptionPayment)
if payload.IsPublic {
fields.Set("message", payload.Message)
if payload.IsPublic && payload.Message != nil {
fields.Set("message", *payload.Message)
}
if payload.IsSubscriptionPayment && payload.TierName != nil {

View file

@ -54,5 +54,5 @@ func handleFormattedMessage(w http.ResponseWriter, r *http.Request) {
return
}
fmt.Fprint(w, msg)
http.Error(w, msg, http.StatusOK)
}

View file

@ -11,7 +11,7 @@
/**
* 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 {String} [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

View file

@ -0,0 +1,19 @@
/**
* Allows to add filters for custom events created through the customHandler
*
* @returns {Object} Custom filter definitions as `filterKey: {name: "Name", visible: true}`
*/
const customFilters = () => ({})
/**
* Handles custom events and creates feed items from them
*
* @param {*} param0 Event-Object as returned by the websocket
* @returns {Object} Event to add to the event list of the feed
*/
const customHandler = eventObj => {
console.log('custom event unhandled:', eventObj)
return null
}
export { customFilters, customHandler }

View file

@ -0,0 +1,156 @@
<html data-bs-theme="dark">
<head>
<title>Event-Feed</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/combine/npm/bootstrap@5.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.5/css/all.min.css">
<style>
[v-cloak] { display: none; }
.border-event {
border-left-width: 5px !important;
border-left-style: solid !important;
border-left-color: #9147ff;
}
.border-event.event-bits { border-left-color: #5cffbe !important; }
.border-event.event-channelpoint { border-left-color: #ffd37a !important; }
.border-event.event-follow { border-left-color: #ff38db !important; }
.border-event.event-raid { border-left-color: #ebeb00 !important; }
.border-event.event-streamOffline { border-left-color: rgb(var(--bs-danger-rgb)) !important; }
.border-event.event-subs { border-left-color: #1f69ff !important; }
.m50 {
max-height: 40vh;
overflow-y: auto;
}
.premono {
font-family: monospace;
font-size: 0.9em;
white-space: pre-wrap;
}
</style>
</head>
<body>
<div id="app" v-cloak>
<div class="container-fluid py-3">
<div class="row">
<div class="col">
<!-- Stream-Summary -->
<div class="card mb-3">
<div class="card-body">
<div class="d-flex align-items-center justify-content-between">
<span
v-for="item in sortedStats"
class="me-2 d-inline-flex align-items-center"
:key="item.key"
>
<i :class="`fa-fw ${item.icon}`"></i>
<span class="badge rounded-pill text-bg-primary ms-1">
{{ item.value }}
</span>
</span>
</div>
</div>
</div>
<!-- Event-List -->
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
Recent events
<div class="btn-group btn-group-sm">
<div class="btn-group btn-group-sm">
<button
type="button"
class="btn btn-secondary dropdown-toggle"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<i class="fas fa-filter fa-fw me-1"></i>
Filters ({{ filterCount }})
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li
v-for="(filter, filterKey) in filters"
:key="filterKey"
>
<a
:class="{'dropdown-item': true, 'active': filter.visible}" href="#"
@click.prevent="toggleFilterVisibility(filterKey)"
>
{{ filter.name }}
</a>
</li>
</ul>
</div>
<button
class="btn btn-secondary"
@click="markRead"
>
<i class="fas fa-eye fa-fw me-1"></i>
Mark read
</button>
</div>
</div>
<div class="list-group list-group-flush">
<!-- Active Hypetrain pin -->
<div class="list-group-item" v-if="hypetrain.active">
<div class="d-flex w-100 align-items-center">
<h5 class="mb-0">
<i :class="`fas fa-train fa-fw me-2`"></i>
Hypetrain in progress towards Level {{ hypetrain.level }}…
</h5>
</div>
<div class="progress my-3">
<div class="progress-bar progress-bar-striped"
:style="`width: ${(hypetrain.progress * 100).toFixed(2)}%`"
></div>
</div>
</div>
<!-- Event-Item -->
<div
:class="eventClass(event)"
v-for="event in recentEvents"
:key="event.time.getTime()"
>
<div class="d-flex w-100 align-items-center">
<h5 class="mb-0 me-auto"><i :class="`${event.icon} fa-fw me-2`"></i> {{ event.title }}</h5>
<button
class="btn btn-sm me-1"
v-if="event.hasReplay"
@click="repeatEvent(event.eventId)"
title="Re-Play Event"
>
<i class="fas fa-share fa-fw"></i>
</button>
<small :title="timeDisplay(event.time)">
{{ timeSince(event.time) }}
</small>
</div>
<div class="d-flex my-1 w-100 justify-content-between align-items-start premono" v-if="event.text">
{{ event.text }}
</div>
<p class="mb-1" v-if="resolveSubtext(event.subtext)">
<small>
<span class="premono">{{ resolveSubtext(event.subtext) }}</span>
</small>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="eventfeed.js" type="module"></script>
</body>
</html>

View file

@ -0,0 +1,585 @@
/**
* @typedef {Object} Event
* @property {number} eventId ID of the event as returned by the server
* @property {Object|undefined} extraData Any additional data specific to this event type
* @property {string} filterKey Event-Type key
* @property {string|undefined} originId ID from the Twitch server for de-duplication
* @property {string|function|undefined} subtext Additional text, usually user-message
* @property {string|undefined} text Descriptive text of the event
* @property {Date} time The moment the event occurred
* @property {string} title The title of the event
* @property {boolean} hasReplay Whether the replay button should be shown
* @property {boolean} isMeta Whether not to display event in frontend
*/
import { customFilters, customHandler } from './eventfeed.custom.js'
import { createApp } from 'https://cdn.jsdelivr.net/npm/vue@3.4/dist/vue.esm-browser.prod.js'
import dayjs from 'https://cdn.jsdelivr.net/npm/dayjs@1.11/+esm'
import dayjsLocalizedFormat from 'https://cdn.jsdelivr.net/npm/dayjs@1.11/plugin/localizedFormat.js/+esm'
import dayjsRelativeTime from 'https://cdn.jsdelivr.net/npm/dayjs@1.11/plugin/relativeTime.js/+esm'
import EventClient from './eventclient.mjs'
const STORAGE_KEY = 'io.luzifer.eventfeed'
const defaultFilters = {
adbreak: { name: 'Adbreaks', visible: true },
ban: { name: 'Bans / Timeouts', visible: true },
bits: { name: 'Bits', visible: true },
channelpoint: { name: 'Channel-Points', visible: true },
donation: { name: 'Donations', visible: true },
follow: { name: 'Follows', visible: true },
hypetrain: { name: 'Hypetrains', visible: true },
pollEnd: { name: 'Poll-Summary', visible: true },
raid: { name: 'Raids', visible: true },
shoutout: { name: 'Shoutouts', visible: true },
streamOffline: { name: 'Stream-Offline', visible: true },
streamUpdate: { name: 'Stream-Update', visible: true },
subs: { name: 'Subs', visible: true },
watchStreak: { name: 'Watchstreaks', visible: true },
}
const userAnonSubgifter = 'ananonymousgifter'
const userAnonCheerer = 'ananonymouscheerer'
const app = createApp({
computed: {
filterCount() {
const filters = Object.values(this.filters)
return `${filters.filter(f => f.visible).length} / ${filters.length}`
},
filters() {
return Object.fromEntries(Object.entries({
...defaultFilters,
...customFilters(),
...this.storedData.filters || {},
})
.filter(e => Object.keys(defaultFilters).includes(e[0]) || Object.keys(customFilters()).includes(e[0]))
.sort((a, b) => a[1].name.localeCompare(b[1].name)))
},
hypetrain() {
const evts = [...this.events]
.filter(evt => evt.filterKey === 'hypetrain')
.sort((b, a) => a.time.getTime() - b.time.getTime())
if (evts.length < 1) {
return {
active: false,
}
}
return evts[0].extraData
},
recentEvents() {
return [...this.events]
.filter(evt => !evt.isMeta)
.filter(evt => this.filters[evt.filterKey]?.visible !== false)
.filter(evt => !this.knownMultiGiftIDs.includes(evt.originId))
.sort((b, a) => a.time.getTime() - b.time.getTime())
},
sortedStats() {
const evts = [...this.events]
.filter(evt => evt.time.getTime() > this.streamOfflineTime.getTime())
return [
{
icon: 'fas fa-gem',
key: 'bits',
value: evts
.filter(evt => evt.filterKey === 'bits')
.reduce((sum, evt) => sum + evt.extraData.bits, 0),
},
{
icon: 'fas fa-circle-dollar-to-slot',
key: 'donation',
value: evts
.filter(evt => evt.filterKey === 'donation')
.reduce((sum, evt) => sum + evt.extraData.amount, 0)
.toFixed(2),
},
{
icon: 'fas fa-heart',
key: 'follow',
value: evts
.filter(evt => evt.filterKey === 'follow')
.length,
},
{
icon: 'fas fa-parachute-box',
key: 'raid',
value: evts
.filter(evt => evt.filterKey === 'raid')
.length,
},
{
icon: 'fas fa-star',
key: 'sub',
value: evts
.filter(evt => evt.filterKey === 'subs')
.filter(evt => !this.knownMultiGiftIDs.includes(evt.originId))
.reduce((sum, evt) => sum + evt.extraData.count, 0),
},
]
},
},
created() {
window.setInterval(() => {
this.now = new Date()
}, 60000)
this.eventClient = new EventClient({
handlers: {
adbreak_begin: ({ event_id, fields, time }) => this.handleAdBreak(event_id, fields, time),
ban: ({ event_id, fields, time }) => this.handleBan(event_id, fields, time),
bits: ({ event_id, fields, time }) => this.handleBits(event_id, fields, time),
category_update: ({ event_id, fields, time }) => this.handleCategoryUpdate(event_id, fields, time),
channelpoint_redeem: ({ event_id, fields, time }) => this.handleChannelPoints(event_id, fields, time),
custom: eventobj => this.handleCustom(eventobj),
follow: ({ event_id, fields, time }) => this.handleFollow(event_id, fields, time),
hypetrain_begin: ({ event_id, fields, time }) => this.handleHypetrain(event_id, fields, time, 'start'),
hypetrain_end: ({ event_id, fields, time }) => this.handleHypetrain(event_id, fields, time, 'end'),
hypetrain_progress: ({ event_id, fields, time }) => this.handleHypetrain(event_id, fields, time, 'progress'),
kofi_donation: ({ event_id, fields, time }) => this.handleKoFiDonation(event_id, fields, time),
poll_end: ({ event_id, fields, time }) => this.handlePollEnd(event_id, fields, time),
raid: ({ event_id, fields, time }) => this.handleRaid(event_id, fields, time),
resub: ({ event_id, fields, reason, time, type }) => this.handleSub(type, event_id, fields, time, reason),
shoutout_created: ({ event_id, fields, time }) => this.handleShoutoutCreated(event_id, fields, time),
shoutout_received: ({ event_id, fields, time }) => this.handleShoutoutReceived(event_id, fields, time),
stream_offline: ({ event_id, time }) => this.handleStreamOffline(event_id, time),
sub: ({ event_id, fields, time, type }) => this.handleSub(type, event_id, fields, time),
subgift: ({ event_id, fields, time, type }) => this.handleSubgift(type, event_id, fields, time),
submysterygift: ({ event_id, fields, time, type }) => this.handleSubgift(type, event_id, fields, time),
timeout: ({ event_id, fields, time }) => this.handleTimeout(event_id, fields, time),
title_update: ({ event_id, fields, time }) => this.handleTitleUpdate(event_id, fields, time),
watch_streak: ({ event_id, fields, time }) => this.handleWatchStreak(event_id, fields, time),
},
maxReplayAge: 168,
replay: true,
})
this.storageLoad()
window.addEventListener('storage', ev => {
if (ev.key !== this.storageKey()) {
return
}
// Our key has been changed, reload stored data
this.storageLoad()
})
},
data() {
return {
eventClient: null,
events: [],
now: new Date(),
storedData: {},
// Workaround for Twitch not sending hypetrain progress in end-event
// eslint-disable-next-line sort-keys
hypetrainProgress: 0,
knownMultiGiftIDs: [],
streamOfflineTime: new Date(0),
subgiftRecipients: {},
}
},
methods: {
/**
* @param {Event} event
*/
addEvent(event) {
if (!event.eventId || !event.filterKey || !event.time || !event.title) {
throw new Error(`Event missing fields: ${event}`)
}
this.events = [
...this.events.filter(evt => evt.eventId !== event.eventId),
event,
]
},
eventClass(event) {
const classes = ['border-event', 'list-group-item']
if (this.storedData.readDate && this.storedData.readDate > event.time.getTime()) {
classes.push('disabled')
}
if (event.filterKey) {
classes.push(`event-${event.filterKey}`)
}
return classes.join(' ')
},
handleAdBreak(eventId, data, time) {
this.addEvent({
eventId,
filterKey: 'adbreak',
icon: 'fas fa-rectangle-ad text-warning',
text: `${data.duration}s ad-break is now running`,
time: time ? new Date(time) : null,
title: 'Ad-Break started',
})
},
handleBan(eventId, data, time) {
this.addEvent({
eventId,
filterKey: 'ban',
icon: 'fas fa-ban',
time: new Date(time),
title: `${data.target_name} has been banned`,
})
},
handleBits(eventId, data, time) {
const from = data.user === userAnonCheerer ? 'Someone' : data.user
this.addEvent({
eventId,
extraData: { bits: data.bits },
filterKey: 'bits',
hasReplay: true,
icon: 'fas fa-gem',
subtext: data.message,
text: `${from} just spent ${data.bits} Bits`,
time: time ? new Date(time) : null,
title: 'Bits donated',
})
},
handleCategoryUpdate(eventId, data, time) {
this.addEvent({
eventId,
filterKey: 'streamUpdate',
icon: 'fas fa-gamepad',
text: data.category,
time: new Date(time),
title: 'Category updated',
})
},
handleChannelPoints(eventId, data, time) {
this.addEvent({
eventId,
filterKey: 'channelpoint',
hasReplay: true,
icon: 'fas fa-diamond',
subtext: data.user_input,
text: `${data.user} redeemed "${data.reward_title}"`,
time: new Date(time),
title: 'Reward Redeemed',
})
},
handleCustom(eventObj) {
const evt = customHandler(eventObj)
if (evt !== null) {
this.addEvent(evt)
}
},
handleFollow(eventId, data, time) {
this.addEvent({
eventId,
filterKey: 'follow',
hasReplay: true,
icon: 'fas fa-user',
text: `${data.user} just followed`,
time: new Date(time),
title: 'New Follower',
})
},
handleHypetrain(eventId, data, time, phase) {
const evt = {
eventId,
extraData: {
active: phase !== 'end',
level: data.level,
progress: data.levelProgress || this.hypetrainProgress,
},
filterKey: 'hypetrain',
icon: 'fas fa-train',
time: new Date(time),
}
this.hypetrainProgress = evt.extraData.progress
switch (phase) {
case 'start':
this.addEvent({
...evt,
text: `A hypetrain started on ${(data.levelProgress * 100).toFixed(0)}% towards level ${data.level}`,
title: 'Hypetrain started',
})
break
case 'progress':
this.addEvent({
...evt,
isMeta: true,
title: 'Hypetrain progressed',
})
break
case 'end':
this.addEvent({
...evt,
text: `A hypetrain ended on ${(this.hypetrainProgress * 100).toFixed(0)}% towards level ${data.level}`,
title: 'Hypetrain ended',
})
break
}
},
handleKoFiDonation(eventId, data, time) {
let text
if (data.isSubscription && data.isFirstSubPayment) {
text = `${data.from} just started a monthly subscription of ${Number(data.amount).toFixed(2)} ${data.currency}`
} else if (data.isSubscription && !data.isFirstSubPayment) {
text = `${data.from} continued their monthly subscription of ${Number(data.amount).toFixed(2)} ${data.currency}`
} else {
text = `${data.from} just donated ${Number(data.amount).toFixed(2)} ${data.currency}`
}
this.addEvent({
eventId,
extraData: { amount: Number(data.amount) },
filterKey: 'donation',
icon: 'fas fa-circle-dollar-to-slot',
subtext: data.message ? data.message : undefined,
text,
time: new Date(time),
title: 'Ko-fi Donation received',
})
},
handlePollEnd(eventId, data, time) {
if (data.poll.status === 'archived') {
return
}
this.addEvent({
eventId,
filterKey: 'pollEnd',
icon: 'fas fa-square-poll-vertical',
subtext: data.poll.choices.map(choice => `${choice.title} (${choice.votes})`).join(' | '),
text: data.poll.title,
time: new Date(time),
title: `Poll Ended (${data.poll.status})`,
})
},
handleRaid(eventId, data, time) {
this.addEvent({
eventId,
filterKey: 'raid',
hasReplay: true,
icon: 'fas fa-parachute-box',
soundUrl: '/public/fanfare.webm',
text: `${data.from} just raided with ${data.viewercount} raiders`,
time: new Date(time),
title: 'Incoming raid',
})
},
handleShoutoutCreated(eventId, data, time) {
this.addEvent({
eventId,
filterKey: 'shoutout',
icon: 'fas fa-bullhorn',
text: `We gave a shoutout for ${data.to} to ${data.viewers} viewers`,
time: new Date(time),
title: 'Shoutout created',
})
},
handleShoutoutReceived(eventId, data, time) {
this.addEvent({
eventId,
filterKey: 'shoutout',
icon: 'fas fa-bullhorn',
text: `${data.from} just gave us a shoutout to ${data.viewers} viewers`,
time: new Date(time),
title: 'Shoutout received',
})
},
handleStreamOffline(eventId, time) {
this.addEvent({
eventId,
filterKey: 'streamOffline',
icon: 'fas fa-clapperboard text-danger',
time: new Date(time),
title: 'Stream Offline',
})
this.streamOfflineTime = new Date(time)
},
handleSub(evt, eventId, data, time) {
const text = evt === 'resub' ? `resubscribed for the ${data.subscribed_months}. time` : 'subscribed'
const tier = data.plan === 'Prime' ? 'P' : `T${Number(data.plan) / 1000}`
const title = evt === 'resub' ? `Resub (${tier})` : `New Sub (${tier})`
this.addEvent({
eventId,
extraData: { count: 1 },
filterKey: 'subs',
hasReplay: true,
icon: 'fas fa-star',
subtext: data.message,
text: `${data.user} just ${text} (${tier})`,
time: new Date(time),
title,
})
},
handleSubgift(evt, eventId, data, time) {
const from = data.user === userAnonSubgifter ? 'ANON' : data.from
const tier = data.plan === 'Prime' ? 'Prime' : `Tier ${Number(data.plan) / 1000}`
if (evt === 'submysterygift') {
this.addEvent({
eventId,
extraData: { count: data.number },
filterKey: 'subs',
hasReplay: true,
icon: 'fas fa-gift',
subtext: () => this.subgiftRecipients[data.origin_id] ? `To: ${this.subgiftRecipients[data.origin_id].join(', ')}` : undefined,
text: `${from} just gifted ${data.number} subs`,
time: time ? new Date(time) : null,
title: `Subs gifted (${tier})`,
variant: 'warning',
})
this.knownMultiGiftIDs.push(data.origin_id)
return
}
if (data.origin_id) {
this.subgiftRecipients[data.origin_id] = [
...this.subgiftRecipients[data.origin_id] || [],
data.to,
].sort((a, b) => a.localeCompare(b))
}
this.addEvent({
eventId,
extraData: { count: 1 },
filterKey: 'subs',
hasReplay: true,
icon: 'fas fa-gift',
originId: data.origin_id,
text: `${from} just gifted ${data.to} a sub`,
time: time ? new Date(time) : null,
title: `Sub gifted (${tier})`,
variant: 'warning',
})
},
handleTimeout(eventId, data, time) {
this.addEvent({
eventId,
filterKey: 'ban',
icon: 'fas fa-ban',
time: new Date(time),
title: `${data.target_name} has been timed out for ${data.seconds}s`,
})
},
handleTitleUpdate(eventId, data, time) {
this.addEvent({
eventId,
filterKey: 'streamUpdate',
icon: 'fas fa-heading',
text: data.title,
time: new Date(time),
title: 'Title updated',
})
},
handleWatchStreak(eventId, data, time) {
this.addEvent({
eventId,
filterKey: 'watchStreak',
icon: 'fas fa-circle-info',
subtext: data.message,
text: `${data.user} watched ${data.streak} consecutive streams`,
time: new Date(time),
title: 'Watch-Streak shared',
})
},
markRead() {
this.storedData.readDate = new Date().getTime()
this.storageSave()
},
repeatEvent(eventId) {
return this.eventClient.replayEvent(eventId)
},
resolveSubtext(subtext) {
if (typeof subtext === 'function') {
return subtext()
}
return subtext
},
storageKey() {
const channel = this.eventClient.paramOptionFallback('channel').replace(/^#*/, '')
return [STORAGE_KEY, channel].join('.')
},
storageLoad() {
this.storedData = {
// Default values
filters: {},
readDate: 0,
// Stored data
...JSON.parse(window.localStorage.getItem(this.storageKey()) || '{}'),
}
},
storageSave() {
window.localStorage.setItem(this.storageKey(), JSON.stringify(this.storedData))
},
timeDisplay(time) {
return dayjs(time).format('llll')
},
timeSince(time) {
return dayjs(time).from(this.now)
},
toggleFilterVisibility(filter) {
if (!this.storedData.filters[filter]) {
this.storedData.filters[filter] = this.filters[filter]
}
this.storedData.filters[filter].visible = !this.storedData.filters[filter].visible
this.storageSave()
},
},
name: 'EventFeed',
})
dayjs.extend(dayjsLocalizedFormat)
dayjs.extend(dayjsRelativeTime)
app.mount('#app')

View file

@ -42,7 +42,7 @@ type (
// socketMessage represents the message overlay sockets will receive
socketMessage struct {
EventID uint64 `json:"event_id"`
EventID uint64 `json:"event_id,string"`
IsLive bool `json:"is_live"`
Reason sendReason `json:"reason"`
Time time.Time `json:"time"`

View file

@ -28,7 +28,7 @@ type (
}
raffle struct {
ID uint64 `gorm:"primaryKey" json:"id"`
ID uint64 `gorm:"primaryKey" json:"id,string"`
Channel string `json:"channel"`
Keyword string `json:"keyword"`
@ -67,7 +67,7 @@ type (
}
raffleEntry struct {
ID uint64 `gorm:"primaryKey" json:"id"`
ID uint64 `gorm:"primaryKey" json:"id,string"`
RaffleID uint64 `gorm:"uniqueIndex:user_per_raffle" json:"-"`
UserID string `gorm:"size:128;uniqueIndex:user_per_raffle" json:"userID"`

View file

@ -57,7 +57,7 @@ func (cryptRandSrc) Int63() int64 {
return -1
}
// mask off sign bit to ensure positive number
return int64(binary.LittleEndian.Uint64(b[:]) & (1<<63 - 1))
return int64(binary.LittleEndian.Uint64(b[:]) & (1<<63 - 1)) //#nosec:G115 - Masking ensures conversion is fine
}
// We're using a non-seedable source

View file

@ -18,23 +18,23 @@ func testGenerateRaffe() raffle {
}
// Now lets generate 132 non-followers taking part
for i := 0; i < 132; i++ {
r.Entries = append(r.Entries, raffleEntry{ID: uint64(i), Multiplier: 1})
for i := uint64(0); i < 132; i++ {
r.Entries = append(r.Entries, raffleEntry{ID: i, Multiplier: 1})
}
// Now lets generate 500 followers taking part
for i := 0; i < 500; i++ {
r.Entries = append(r.Entries, raffleEntry{ID: 10000 + uint64(i), Multiplier: r.MultiFollower})
for i := uint64(0); i < 500; i++ {
r.Entries = append(r.Entries, raffleEntry{ID: 10000 + i, Multiplier: r.MultiFollower})
}
// Now lets generate 200 subscribers taking part
for i := 0; i < 200; i++ {
r.Entries = append(r.Entries, raffleEntry{ID: 20000 + uint64(i), Multiplier: r.MultiSubscriber})
for i := uint64(0); i < 200; i++ {
r.Entries = append(r.Entries, raffleEntry{ID: 20000 + i, Multiplier: r.MultiSubscriber})
}
// Now lets generate 5 VIPs taking part
for i := 0; i < 5; i++ {
r.Entries = append(r.Entries, raffleEntry{ID: 30000 + uint64(i), Multiplier: r.MultiVIP})
for i := uint64(0); i < 5; i++ {
r.Entries = append(r.Entries, raffleEntry{ID: 30000 + i, Multiplier: r.MultiVIP})
}
// They didn't join in order so lets shuffle them

View file

@ -57,13 +57,12 @@ func TestScanForLinks(t *testing.T) {
t.SkipNow()
}
c := New()
for _, testCase := range []struct {
Heuristic bool
Message string
ExpectedLinks []string
ExpectedContains bool
TraceStack bool
}{
// Case: full URL is present in the message
{
@ -183,6 +182,13 @@ func TestScanForLinks(t *testing.T) {
{Heuristic: false, Message: "Hey btw. es kann sein, dass", ExpectedLinks: nil},
} {
t.Run(fmt.Sprintf("h:%v lc:%d m:%s", testCase.Heuristic, len(testCase.ExpectedLinks), testCase.Message), func(t *testing.T) {
var c *Checker
if testCase.TraceStack {
c = New(withResolver(newResolver(resolverPoolSize, withTesting(t))))
} else {
c = New()
}
var linksFound []string
if testCase.Heuristic {
linksFound = c.HeuristicScanForLinks(testCase.Message)
@ -209,23 +215,3 @@ func TestScanForLinks(t *testing.T) {
})
}
}
func TestUserAgentListNotEmpty(t *testing.T) {
if len(defaultUserAgents) == 0 {
t.Fatal("found empty user-agent list")
}
}
func TestUserAgentRandomizer(t *testing.T) {
uas := map[string]int{}
for i := 0; i < 10; i++ {
uas[defaultResolver.userAgent()]++
}
for _, c := range uas {
assert.Less(t, c, 10)
}
assert.Equal(t, 0, uas[""]) // there should be no empty UA
}

View file

@ -0,0 +1,66 @@
package linkcheck
import (
"bytes"
"errors"
"fmt"
"io"
"regexp"
"golang.org/x/net/html"
)
var (
errNoMetaRedir = fmt.Errorf("no meta-redir found")
metaRedirContent = regexp.MustCompile(`^[0-9]+;\s*url=(.*)$`)
)
//nolint:gocognit // Makes no sense to split
func resolveMetaRedirect(body []byte) (redir string, err error) {
tok := html.NewTokenizer(bytes.NewReader(body))
tokenLoop:
for {
token := tok.Next()
switch token {
case html.ErrorToken:
if errors.Is(tok.Err(), io.EOF) {
break tokenLoop
}
return "", fmt.Errorf("scanning tokens: %w", tok.Err())
case html.StartTagToken:
t := tok.Token()
if t.Data == "meta" {
var (
content string
isRedirect bool
)
for _, attr := range t.Attr {
isRedirect = isRedirect || attr.Key == "http-equiv" && attr.Val == "refresh"
if attr.Key == "content" {
content = attr.Val
}
}
if !isRedirect {
continue tokenLoop
}
// It is a redirect, get the content and parse it
if matches := metaRedirContent.FindStringSubmatch(content); len(matches) > 1 {
redir = matches[1]
}
}
}
}
if redir == "" {
// We did not find anything
return "", errNoMetaRedir
}
return redir, nil
}

View file

@ -0,0 +1,41 @@
package linkcheck
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestResolveMetaRedir(t *testing.T) {
testDoc := []byte(`<!DOCTYPE html>
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta property="twitter:image" content="">
<meta http-equiv='refresh' content='0; url=https://github.com/Luzifer/twitch-bot'>
</head>
<body>
</body>
</html>`)
redir, err := resolveMetaRedirect(testDoc)
require.NoError(t, err)
assert.Equal(t, "https://github.com/Luzifer/twitch-bot", redir)
testDoc = []byte(`<!DOCTYPE html>
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta property="twitter:image" content="">
</head>
<body>
</body>
</html>`)
redir, err = resolveMetaRedirect(testDoc)
require.ErrorIs(t, err, errNoMetaRedir)
assert.Equal(t, "", redir)
}

View file

@ -2,18 +2,16 @@ package linkcheck
import (
"context"
"crypto/rand"
_ "embed"
"math/big"
"io"
"net/http"
"net/http/cookiejar"
"net/url"
"regexp"
"strings"
"sync"
"testing"
"time"
"github.com/Luzifer/go_helpers/v2/str"
"github.com/sirupsen/logrus"
)
@ -30,6 +28,8 @@ type (
resolver struct {
resolverC chan resolverQueueEntry
skipValidation bool
t *testing.T
}
resolverQueueEntry struct {
@ -40,20 +40,12 @@ type (
)
var (
defaultUserAgents = []string{}
linkTest = regexp.MustCompile(`(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]`)
numericHost = regexp.MustCompile(`^(?:[0-9]+\.)*[0-9]+(?::[0-9]+)?$`)
//go:embed user-agents.txt
uaList string
linkTest = regexp.MustCompile(`(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]`)
numericHost = regexp.MustCompile(`^(?:[0-9]+\.)*[0-9]+(?::[0-9]+)?$`)
defaultResolver = newResolver(resolverPoolSize)
)
func init() {
defaultUserAgents = strings.Split(strings.TrimSpace(uaList), "\n")
}
func newResolver(poolSize int, opts ...func(*resolver)) *resolver {
r := &resolver{
resolverC: make(chan resolverQueueEntry),
@ -74,6 +66,10 @@ func withSkipVerify() func(*resolver) {
return func(r *resolver) { r.skipValidation = true }
}
func withTesting(t *testing.T) func(*resolver) {
return func(r *resolver) { r.t = t }
}
func (r resolver) Resolve(qe resolverQueueEntry) {
qe.WaitGroup.Add(1)
r.resolverC <- qe
@ -87,13 +83,13 @@ 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 {
//nolint:funlen,gocyclo
func (r resolver) resolveFinal(link string, cookieJar *cookiejar.Jar, callStack *stack) string {
if !linkTest.MatchString(link) && !r.skipValidation {
return ""
}
if str.StringInSlice(link, callStack) || len(callStack) == maxRedirects {
if callStack.Count(link) > 2 || callStack.Height() == maxRedirects {
// We got ourselves a loop: Yay!
return link
}
@ -131,12 +127,19 @@ func (r resolver) resolveFinal(link string, cookieJar *cookiejar.Jar, callStack
// Sanitize host: Trailing dots are valid but not required
u.Host = strings.TrimRight(u.Host, ".")
if r.t != nil {
r.t.Logf("resolving link: link=%q jar_c=%#v stack_c=%d stack_h=%d",
link, len(cookieJar.Cookies(u)), callStack.Count(link), callStack.Height())
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return ""
}
req.Header.Set("User-Agent", userAgent)
for k, v := range generateUserAgentHeaders() {
req.Header.Set(k, v)
}
resp, err := client.Do(req)
if err != nil {
@ -155,10 +158,35 @@ func (r resolver) resolveFinal(link string, cookieJar *cookiejar.Jar, callStack
return ""
}
target := r.resolveReference(u, tu)
return r.resolveFinal(target, cookieJar, append(callStack, link), userAgent)
callStack.Visit(link)
return r.resolveFinal(target, cookieJar, callStack)
}
// We got a response, it's no redirect, we count this as a success
// We got a response, it's no redirect, lets check for in-document stuff
docBody, err := io.ReadAll(resp.Body)
if err != nil {
return ""
}
if metaRedir, err := resolveMetaRedirect(docBody); err == nil {
// Meta-Redirect found
tu, err := url.Parse(metaRedir)
if err != nil {
return ""
}
target := r.resolveReference(u, tu)
callStack.Visit(link)
return r.resolveFinal(target, cookieJar, callStack)
}
if resp.Header.Get("Set-Cookie") != "" {
// A new cookie was set, lets refresh the page once to see if stuff
// changes with that new cookie
callStack.Visit(link)
return r.resolveFinal(u.String(), cookieJar, callStack)
}
// We had no in-document redirects: we count this as a success
return u.String()
}
@ -201,14 +229,9 @@ func (resolver) resolveReference(origin *url.URL, loc *url.URL) string {
func (r resolver) runResolver() {
for qe := range r.resolverC {
if link := r.resolveFinal(qe.Link, r.getJar(), nil, r.userAgent()); link != "" {
if link := r.resolveFinal(qe.Link, r.getJar(), &stack{}); link != "" {
qe.Callback(link)
}
qe.WaitGroup.Done()
}
}
func (resolver) userAgent() string {
n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(defaultUserAgents))))
return defaultUserAgents[n.Int64()]
}

View file

@ -0,0 +1,27 @@
package linkcheck
import "strings"
type (
stack struct {
visits []string
}
)
func (s stack) Count(url string) (n int) {
for _, v := range s.visits {
if strings.EqualFold(v, url) {
n++
}
}
return n
}
func (s stack) Height() int {
return len(s.visits)
}
func (s *stack) Visit(url string) {
s.visits = append(s.visits, url)
}

View file

@ -1,43 +0,0 @@
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36
Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/110.0
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.57
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.3 Safari/605.1.15
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 OPR/95.0.0.0
Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36
Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/110.0
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.41
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.56
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.53 Safari/537.36 Edg/103.0.1264.37
Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.85 Safari/537.36 Edg/90.0.818.46
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.50
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Whale/3.19.166.16 Safari/537.36
Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/110.0
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 Edg/108.0.1462.76
Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.46
Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:109.0) Gecko/20100101 Firefox/110.0
Mozilla/5.0 (Windows NT 6.3; Win64; x64; rv:109.0) Gecko/20100101 Firefox/110.0
Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36 Core/1.94.192.400 QQBrowser/11.5.5250.400
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.1518.78
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36
Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 OPR/95.0.0.0
Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36 Edg/92.0.902.67
Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:108.0) Gecko/20100101 Firefox/108.0
Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36
Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/18.17763
Mozilla/5.0 (X11; Linux x86_64; rv:108.0) Gecko/20100101 Firefox/108.0
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36
Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63
Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 Edg/108.0.1462.54
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.1518.61
Mozilla/5.0 (Windows NT 10.0; rv:109.0) Gecko/20100101 Firefox/110.0
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.1518.70

View file

@ -0,0 +1,38 @@
package linkcheck
import (
"fmt"
)
const (
chromeMajor = 128
webkitMajor = 537
webkitMinor = 36
)
// generateUserAgent resembles the Chrome user agent generation as
// closely as possible in order to blend into the crowd of browsers
//
// https://github.com/chromium/chromium/blob/58e23d958ee8d2bb4b085c843a18eb28b9da17da/content/common/user_agent.cc
func generateUserAgentHeaders() map[string]string {
return map[string]string{
// New UA hints method
"Sec-CH-UA": fmt.Sprintf(
`"Chromium";v="%[1]d", "Not;A=Brand";v="24", "Google Chrome";v="%[1]d"`,
chromeMajor,
),
// Not a mobile browser
"Sec-CH-UA-Mobile": "?0",
// We're always Windows
"Sec-CH-UA-Platform": "Windows",
// "old" user-agent
"User-Agent": fmt.Sprintf(
"Mozilla/5.0 (%s) AppleWebKit/537.36 (KHTML, like Gecko) %s Safari/537.36",
"Windows NT 10.0; Win64; x64", // We're always Windows 10 / 11 on x64
fmt.Sprintf("Chrome/%d.0.0.0", chromeMajor), // UA-Reduction enabled
),
}
}

50
internal/locker/locker.go Normal file
View file

@ -0,0 +1,50 @@
// Package locker contains a way to interact with arbitrary locks
package locker
import "sync"
var (
locks = map[string]*sync.RWMutex{}
locksOLocks sync.RWMutex
)
// LockByKey takes a key to lock and locks the corresponding RWMutex
func LockByKey(key string) { getLockByKey(key).Lock() }
// RLockByKey takes a key to lock and read-locks the corresponding RWMutex
func RLockByKey(key string) { getLockByKey(key).RLock() }
// RUnlockByKey takes a key to lock and read-unlocks the corresponding RWMutex
func RUnlockByKey(key string) { getLockByKey(key).RUnlock() }
// UnlockByKey takes a key to lock and unlocks the corresponding RWMutex
func UnlockByKey(key string) { getLockByKey(key).Unlock() }
// WithLock takes a key to lock and a function to execute during the
// lock of this key
func WithLock(key string, fn func()) {
LockByKey(key)
defer UnlockByKey(key)
fn()
}
// WithRLock takes a key to lock and a function to execute during the
// read-lock of this key
func WithRLock(key string, fn func()) {
RLockByKey(key)
defer RUnlockByKey(key)
fn()
}
func getLockByKey(key string) *sync.RWMutex {
locksOLocks.Lock()
defer locksOLocks.Unlock()
if locks[key] == nil {
locks[key] = new(sync.RWMutex)
}
return locks[key]
}

View file

@ -72,9 +72,7 @@ func (s Service) InCooldown(tt plugins.TimerType, limiter, ruleID string) (bool,
}
func (Service) getCooldownTimerKey(tt plugins.TimerType, limiter, ruleID string) string {
h := sha256.New()
fmt.Fprintf(h, "%d:%s:%s", tt, limiter, ruleID)
return fmt.Sprintf("sha256:%x", h.Sum(nil))
return fmt.Sprintf("sha256:%x", sha256.Sum256([]byte(fmt.Sprintf("%d:%s:%s", tt, limiter, ruleID))))
}
// Permit timer
@ -90,9 +88,10 @@ func (s Service) HasPermit(channel, username string) (bool, error) {
}
func (Service) getPermitTimerKey(channel, username string) string {
h := sha256.New()
fmt.Fprintf(h, "%d:%s:%s", plugins.TimerTypePermit, channel, strings.ToLower(strings.TrimLeft(username, "@")))
return fmt.Sprintf("sha256:%x", h.Sum(nil))
return fmt.Sprintf("sha256:%x", sha256.Sum256([]byte(fmt.Sprintf(
"%d:%s:%s",
plugins.TimerTypePermit, channel, strings.ToLower(strings.TrimLeft(username, "@")),
))))
}
// Generic timer

View file

@ -2,6 +2,9 @@
package date
import (
"fmt"
"time"
"github.com/Luzifer/twitch-bot/v3/plugins"
)
@ -27,5 +30,37 @@ func Register(args plugins.RegistrationArguments) error {
},
})
args.RegisterTemplateFunction("parseDuration", plugins.GenericTemplateFunctionGetter(func(duration string) (time.Duration, error) {
d, err := time.ParseDuration(duration)
if err != nil {
return 0, fmt.Errorf("parsing duration: %w", err)
}
return d, nil
}), plugins.TemplateFuncDocumentation{
Description: `Parses a duration (i.e. 1h25m10s) into a time.Duration`,
Syntax: "parseDuration <duration>",
Example: &plugins.TemplateFuncDocumentationExample{
Template: `{{ parseDuration "1h30s" }}`,
ExpectedOutput: "1h0m30s",
},
})
args.RegisterTemplateFunction("parseDurationToSeconds", plugins.GenericTemplateFunctionGetter(func(duration string) (int64, error) {
d, err := time.ParseDuration(duration)
if err != nil {
return 0, fmt.Errorf("parsing duration: %w", err)
}
return int64(d / time.Second), nil
}), plugins.TemplateFuncDocumentation{
Description: `Parses a duration (i.e. 1h25m10s) into a number of seconds`,
Syntax: "parseDurationToSeconds <duration>",
Example: &plugins.TemplateFuncDocumentationExample{
Template: `{{ parseDurationToSeconds "1h25m10s" }}`,
ExpectedOutput: "5110",
},
})
return nil
}

View file

@ -40,15 +40,15 @@ func NewInterval(a, b time.Time) (i Interval) {
i.Seconds = u.Second() - l.Second()
if i.Seconds < 0 {
i.Minutes, i.Seconds = i.Minutes-1, i.Seconds+60 //nolint:gomnd
i.Minutes, i.Seconds = i.Minutes-1, i.Seconds+60 //nolint:mnd
}
if i.Minutes < 0 {
i.Hours, i.Minutes = i.Hours-1, i.Minutes+60 //nolint:gomnd
i.Hours, i.Minutes = i.Hours-1, i.Minutes+60 //nolint:mnd
}
if i.Hours < 0 {
i.Days, i.Hours = i.Days-1, i.Hours+24 //nolint:gomnd
i.Days, i.Hours = i.Days-1, i.Hours+24 //nolint:mnd
}
if i.Days < 0 {
@ -57,7 +57,7 @@ func NewInterval(a, b time.Time) (i Interval) {
}
if i.Months < 0 {
i.Years, i.Months = i.Years-1, i.Months+12 //nolint:gomnd
i.Years, i.Months = i.Years-1, i.Months+12 //nolint:mnd
}
return i

View file

@ -63,7 +63,7 @@ func stringToSeed(s string) (int64, error) {
)
for i := 0; i < len(hashSum); i++ {
sum += int64(float64(hashSum[len(hashSum)-1-i]%10) * math.Pow(10, float64(i))) //nolint:gomnd // No need to put the 10 of 10**i into a constant named "ten"
sum += int64(float64(hashSum[len(hashSum)-1-i]%10) * math.Pow(10, float64(i))) //nolint:mnd // No need to put the 10 of 10**i into a constant named "ten"
}
return sum, nil

View file

@ -15,6 +15,7 @@ func init() {
regFn,
tplTwitchRecentGame,
tplTwitchRecentTitle,
tplTwitchStreamIsLive,
tplTwitchStreamUptime,
)
}
@ -55,6 +56,20 @@ func tplTwitchRecentTitle(args plugins.RegistrationArguments) {
})
}
func tplTwitchStreamIsLive(args plugins.RegistrationArguments) {
args.RegisterTemplateFunction("streamIsLive", plugins.GenericTemplateFunctionGetter(func(username string) bool {
_, err := args.GetTwitchClient().GetCurrentStreamInfo(context.Background(), strings.TrimLeft(username, "#"))
return err == nil
}), plugins.TemplateFuncDocumentation{
Description: "Check whether a given channel is currently live",
Syntax: "streamIsLive <username>",
Example: &plugins.TemplateFuncDocumentationExample{
Template: `{{ streamIsLive "luziferus" }}`,
FakedOutput: "true",
},
})
}
func tplTwitchStreamUptime(args plugins.RegistrationArguments) {
args.RegisterTemplateFunction("streamUptime", plugins.GenericTemplateFunctionGetter(func(username string) (time.Duration, error) {
si, err := args.GetTwitchClient().GetCurrentStreamInfo(context.Background(), strings.TrimLeft(username, "#"))

View file

@ -14,6 +14,7 @@ func init() {
tplTwitchDisplayName,
tplTwitchIDForUsername,
tplTwitchProfileImage,
tplTwitchUserExists,
tplTwitchUsernameForID,
)
}
@ -68,6 +69,25 @@ func tplTwitchProfileImage(args plugins.RegistrationArguments) {
})
}
func tplTwitchUserExists(args plugins.RegistrationArguments) {
args.RegisterTemplateFunction("userExists", plugins.GenericTemplateFunctionGetter(func(username string) bool {
user, err := args.GetTwitchClient().GetUserInformation(context.Background(), strings.TrimLeft(username, "#@"))
if err != nil {
// Well, they probably don't exist
return false
}
return strings.EqualFold(username, user.Login)
}), plugins.TemplateFuncDocumentation{
Description: "Checks whether the given user exists",
Syntax: "userExists <username>",
Example: &plugins.TemplateFuncDocumentationExample{
Template: `{{ userExists "luziferus" }}`,
FakedOutput: "true",
},
})
}
func tplTwitchUsernameForID(args plugins.RegistrationArguments) {
args.RegisterTemplateFunction("usernameForID", plugins.GenericTemplateFunctionGetter(func(id string) (string, error) {
username, err := args.GetTwitchClient().GetUsernameForID(context.Background(), id)

View file

@ -0,0 +1,50 @@
package twitch
import (
"context"
"fmt"
"strings"
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
"github.com/Luzifer/twitch-bot/v3/plugins"
)
func init() {
regFn = append(
regFn,
tplTwitchCurrentVOD,
)
}
func tplTwitchCurrentVOD(args plugins.RegistrationArguments) {
args.RegisterTemplateFunction("currentVOD", plugins.GenericTemplateFunctionGetter(func(username string) (string, error) {
si, err := args.GetTwitchClient().GetCurrentStreamInfo(context.Background(), strings.TrimLeft(username, "#"))
if err != nil {
return "", fmt.Errorf("getting stream info: %w", err)
}
vids, err := args.GetTwitchClient().GetVideos(context.TODO(), twitch.GetVideoOpts{
UserID: si.UserID,
})
if err != nil {
return "", fmt.Errorf("getting videos: %w", err)
}
for _, v := range vids {
if v.StreamID == nil || *v.StreamID != si.ID {
continue
}
return v.URL, nil
}
return "", fmt.Errorf("no matching VOD found")
}), plugins.TemplateFuncDocumentation{
Description: "Returns the VOD of the currently running stream in the given channel (causes an error if no current stream / VOD is found)",
Syntax: "currentVOD <username>",
Example: &plugins.TemplateFuncDocumentationExample{
Template: `{{ currentVOD .channel }}`,
FakedOutput: "https://www.twitch.tv/videos/123456789",
},
})
}

2
irc.go
View file

@ -294,7 +294,7 @@ func (i ircHandler) handlePermit(m *irc.Message) {
}
msgParts := strings.Split(m.Trailing(), " ")
if len(msgParts) != 2 { //nolint:gomnd // This is not a magic number but just an expected count
if len(msgParts) != 2 { //nolint:mnd // This is not a magic number but just an expected count
return
}

View file

@ -193,7 +193,7 @@ func main() {
}
if len(rconfig.Args()) > 1 {
if err = cli.Call(rconfig.Args()[1:]); err != nil {
if err = cliTool.Call(rconfig.Args()[1:]); err != nil {
log.Fatalf("error in command: %s", err)
}
return

291
package-lock.json generated
View file

@ -4,7 +4,6 @@
"requires": true,
"packages": {
"": {
"name": "twitch-bot",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.4.0",
"@fortawesome/free-brands-svg-icons": "^6.4.0",
@ -16,7 +15,7 @@
"bootswatch": "^4.6.2",
"codejar": "^3.7.0",
"prismjs": "^1.29.0",
"vue": "^2.7.14",
"vue": "^2.7.16",
"vue-router": "^3.6.5"
},
"devDependencies": {
@ -345,19 +344,19 @@
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz",
"integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==",
"dev": true,
"version": "7.24.8",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz",
"integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
"dev": true,
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz",
"integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
@ -403,9 +402,13 @@
}
},
"node_modules/@babel/parser": {
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz",
"integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==",
"version": "7.25.3",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz",
"integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==",
"license": "MIT",
"dependencies": {
"@babel/types": "^7.25.2"
},
"bin": {
"parser": "bin/babel-parser.js"
},
@ -451,13 +454,13 @@
}
},
"node_modules/@babel/types": {
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz",
"integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
"dev": true,
"version": "7.25.2",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz",
"integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.22.5",
"@babel/helper-validator-identifier": "^7.22.20",
"@babel/helper-string-parser": "^7.24.8",
"@babel/helper-validator-identifier": "^7.24.7",
"to-fast-properties": "^2.0.0"
},
"engines": {
@ -1184,13 +1187,16 @@
}
},
"node_modules/@vue/compiler-sfc": {
"version": "2.7.14",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.14.tgz",
"integrity": "sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA==",
"version": "2.7.16",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.16.tgz",
"integrity": "sha512-KWhJ9k5nXuNtygPU7+t1rX6baZeqOYLEforUPjgNDBnLicfHCoi48H87Q8XyLZOrNNsmhuwKqtpDQWjEFe6Ekg==",
"dependencies": {
"@babel/parser": "^7.18.4",
"@babel/parser": "^7.23.5",
"postcss": "^8.4.14",
"source-map": "^0.6.1"
},
"optionalDependencies": {
"prettier": "^1.18.2 || ^2.0.0"
}
},
"node_modules/@vue/component-compiler": {
@ -1344,10 +1350,11 @@
"optional": true
},
"node_modules/assert-never": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.2.1.tgz",
"integrity": "sha512-TaTivMB6pYI1kXwrFlEhLeGfOqoDNdTxjCdwRfFFkEA30Eu+k48W34nlok2EYWJfFFzqaEmichdNM7th6M5HNw==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.3.0.tgz",
"integrity": "sha512-9Z3vxQ+berkL/JJo0dK+EY3Lp0s3NtSnP3VCLsh5HDcZPrh0M+KQRK5sWhUeyPPH+/RCxZqOxLMR+YC6vlviEQ==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/asynckit": {
@ -1369,11 +1376,12 @@
}
},
"node_modules/axios": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz",
"integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==",
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
"integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.0",
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
@ -1383,6 +1391,7 @@
"resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz",
"integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@babel/types": "^7.9.6"
@ -1496,13 +1505,14 @@
}
},
"node_modules/braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"fill-range": "^7.0.1"
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
@ -1740,10 +1750,11 @@
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@ -1867,6 +1878,7 @@
"resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz",
"integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/electron-to-chromium": {
@ -2286,10 +2298,11 @@
}
},
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"to-regex-range": "^5.0.1"
@ -2334,15 +2347,16 @@
"dev": true
},
"node_modules/follow-redirects": {
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
@ -2737,6 +2751,7 @@
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.12.0"
@ -2815,6 +2830,7 @@
"resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz",
"integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/js-tokens": {
@ -3072,15 +3088,16 @@
"dev": true
},
"node_modules/nanoid": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@ -3575,7 +3592,6 @@
"version": "2.8.7",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz",
"integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==",
"dev": true,
"optional": true,
"bin": {
"prettier": "bin-prettier.js"
@ -3624,13 +3640,14 @@
"dev": true
},
"node_modules/pug": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/pug/-/pug-3.0.2.tgz",
"integrity": "sha512-bp0I/hiK1D1vChHh6EfDxtndHji55XP/ZJKwsRqrz6lRia6ZC2OZbdAymlxdVFwd1L70ebrVJw4/eZ79skrIaw==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pug/-/pug-3.0.3.tgz",
"integrity": "sha512-uBi6kmc9f3SZ3PXxqcHiUZLmIXgfgWooKWXcwSGwQd2Zi5Rb0bT14+8CJjJgI8AB+nndLaNgHGrcc6bPIB665g==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"pug-code-gen": "^3.0.2",
"pug-code-gen": "^3.0.3",
"pug-filters": "^4.0.0",
"pug-lexer": "^5.0.1",
"pug-linker": "^4.0.0",
@ -3645,6 +3662,7 @@
"resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-3.0.0.tgz",
"integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"constantinople": "^4.0.1",
@ -3653,27 +3671,29 @@
}
},
"node_modules/pug-code-gen": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.2.tgz",
"integrity": "sha512-nJMhW16MbiGRiyR4miDTQMRWDgKplnHyeLvioEJYbk1RsPI3FuA3saEP8uwnTb2nTJEKBU90NFVWJBk4OU5qyg==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.3.tgz",
"integrity": "sha512-cYQg0JW0w32Ux+XTeZnBEeuWrAY7/HNE6TWnhiHGnnRYlCgyAUPoyh9KzCMa9WhcJlJ1AtQqpEYHc+vbCzA+Aw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"constantinople": "^4.0.1",
"doctypes": "^1.1.0",
"js-stringify": "^1.0.2",
"pug-attrs": "^3.0.0",
"pug-error": "^2.0.0",
"pug-runtime": "^3.0.0",
"pug-error": "^2.1.0",
"pug-runtime": "^3.0.1",
"void-elements": "^3.1.0",
"with": "^7.0.0"
}
},
"node_modules/pug-error": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.0.0.tgz",
"integrity": "sha512-sjiUsi9M4RAGHktC1drQfCr5C5eriu24Lfbt4s+7SykztEOwVZtbFk1RRq0tzLxcMxMYTBR+zMQaG07J/btayQ==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.1.0.tgz",
"integrity": "sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/pug-filters": {
@ -3740,6 +3760,7 @@
"resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-3.0.1.tgz",
"integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/pug-strip-comments": {
@ -4134,7 +4155,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
"dev": true,
"engines": {
"node": ">=4"
}
@ -4144,6 +4164,7 @@
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"is-number": "^7.0.0"
@ -4250,17 +4271,20 @@
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/vue": {
"version": "2.7.14",
"resolved": "https://registry.npmjs.org/vue/-/vue-2.7.14.tgz",
"integrity": "sha512-b2qkFyOM0kwqWFuQmgd4o+uHGU7T+2z3T+WQp8UBjADfEv2n4FEMffzBmCKNP0IGzOEEfYjvtcC62xaSKeQDrQ==",
"version": "2.7.16",
"resolved": "https://registry.npmjs.org/vue/-/vue-2.7.16.tgz",
"integrity": "sha512-4gCtFXaAA3zYZdTp5s4Hl2sozuySsgz4jy1EnpBHNfpMa9dK1ZCG7viqBPCwXtmgc8nHqUsAu3G4gtmXkkY3Sw==",
"deprecated": "Vue 2 has reached EOL and is no longer actively maintained. See https://v2.vuejs.org/eol/ for more details.",
"license": "MIT",
"dependencies": {
"@vue/compiler-sfc": "2.7.14",
"@vue/compiler-sfc": "2.7.16",
"csstype": "^3.1.0"
}
},
@ -4308,10 +4332,11 @@
"integrity": "sha512-VYXZQLtjuvKxxcshuRAwjHnciqZVoXAjTjcqBTz4rKc8qih9g9pI3hbDjmqXaHdgL3v8pV6P8Z335XvHzESxLQ=="
},
"node_modules/vue-template-compiler": {
"version": "2.7.14",
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz",
"integrity": "sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==",
"version": "2.7.16",
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz",
"integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"de-indent": "^1.0.2",
"he": "^1.2.0"
@ -4357,6 +4382,7 @@
"resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz",
"integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@babel/parser": "^7.9.6",
@ -4651,16 +4677,14 @@
}
},
"@babel/helper-string-parser": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz",
"integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==",
"dev": true
"version": "7.24.8",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz",
"integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ=="
},
"@babel/helper-validator-identifier": {
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
"dev": true
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz",
"integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w=="
},
"@babel/helper-validator-option": {
"version": "7.22.5",
@ -4694,9 +4718,12 @@
}
},
"@babel/parser": {
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz",
"integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw=="
"version": "7.25.3",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz",
"integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==",
"requires": {
"@babel/types": "^7.25.2"
}
},
"@babel/template": {
"version": "7.22.15",
@ -4730,13 +4757,12 @@
}
},
"@babel/types": {
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz",
"integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
"dev": true,
"version": "7.25.2",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz",
"integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==",
"requires": {
"@babel/helper-string-parser": "^7.22.5",
"@babel/helper-validator-identifier": "^7.22.20",
"@babel/helper-string-parser": "^7.24.8",
"@babel/helper-validator-identifier": "^7.24.7",
"to-fast-properties": "^2.0.0"
}
},
@ -5162,12 +5188,13 @@
}
},
"@vue/compiler-sfc": {
"version": "2.7.14",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.14.tgz",
"integrity": "sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA==",
"version": "2.7.16",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.16.tgz",
"integrity": "sha512-KWhJ9k5nXuNtygPU7+t1rX6baZeqOYLEforUPjgNDBnLicfHCoi48H87Q8XyLZOrNNsmhuwKqtpDQWjEFe6Ekg==",
"requires": {
"@babel/parser": "^7.18.4",
"@babel/parser": "^7.23.5",
"postcss": "^8.4.14",
"prettier": "^1.18.2 || ^2.0.0",
"source-map": "^0.6.1"
}
},
@ -5288,9 +5315,9 @@
"optional": true
},
"assert-never": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.2.1.tgz",
"integrity": "sha512-TaTivMB6pYI1kXwrFlEhLeGfOqoDNdTxjCdwRfFFkEA30Eu+k48W34nlok2EYWJfFFzqaEmichdNM7th6M5HNw==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.3.0.tgz",
"integrity": "sha512-9Z3vxQ+berkL/JJo0dK+EY3Lp0s3NtSnP3VCLsh5HDcZPrh0M+KQRK5sWhUeyPPH+/RCxZqOxLMR+YC6vlviEQ==",
"dev": true,
"optional": true
},
@ -5307,11 +5334,11 @@
"optional": true
},
"axios": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz",
"integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==",
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
"integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
"requires": {
"follow-redirects": "^1.15.0",
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
@ -5397,13 +5424,13 @@
}
},
"braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"optional": true,
"requires": {
"fill-range": "^7.0.1"
"fill-range": "^7.1.1"
}
},
"browserslist": {
@ -5579,9 +5606,9 @@
}
},
"cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"requires": {
"path-key": "^3.1.0",
@ -6003,9 +6030,9 @@
}
},
"fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"optional": true,
"requires": {
@ -6039,9 +6066,9 @@
"dev": true
},
"follow-redirects": {
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw=="
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA=="
},
"form-data": {
"version": "4.0.0",
@ -6606,9 +6633,9 @@
"dev": true
},
"nanoid": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA=="
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="
},
"native-request": {
"version": "1.1.0",
@ -6975,7 +7002,6 @@
"version": "2.8.7",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz",
"integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==",
"dev": true,
"optional": true
},
"prismjs": {
@ -7012,13 +7038,13 @@
"dev": true
},
"pug": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/pug/-/pug-3.0.2.tgz",
"integrity": "sha512-bp0I/hiK1D1vChHh6EfDxtndHji55XP/ZJKwsRqrz6lRia6ZC2OZbdAymlxdVFwd1L70ebrVJw4/eZ79skrIaw==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pug/-/pug-3.0.3.tgz",
"integrity": "sha512-uBi6kmc9f3SZ3PXxqcHiUZLmIXgfgWooKWXcwSGwQd2Zi5Rb0bT14+8CJjJgI8AB+nndLaNgHGrcc6bPIB665g==",
"dev": true,
"optional": true,
"requires": {
"pug-code-gen": "^3.0.2",
"pug-code-gen": "^3.0.3",
"pug-filters": "^4.0.0",
"pug-lexer": "^5.0.1",
"pug-linker": "^4.0.0",
@ -7041,9 +7067,9 @@
}
},
"pug-code-gen": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.2.tgz",
"integrity": "sha512-nJMhW16MbiGRiyR4miDTQMRWDgKplnHyeLvioEJYbk1RsPI3FuA3saEP8uwnTb2nTJEKBU90NFVWJBk4OU5qyg==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.3.tgz",
"integrity": "sha512-cYQg0JW0w32Ux+XTeZnBEeuWrAY7/HNE6TWnhiHGnnRYlCgyAUPoyh9KzCMa9WhcJlJ1AtQqpEYHc+vbCzA+Aw==",
"dev": true,
"optional": true,
"requires": {
@ -7051,16 +7077,16 @@
"doctypes": "^1.1.0",
"js-stringify": "^1.0.2",
"pug-attrs": "^3.0.0",
"pug-error": "^2.0.0",
"pug-runtime": "^3.0.0",
"pug-error": "^2.1.0",
"pug-runtime": "^3.0.1",
"void-elements": "^3.1.0",
"with": "^7.0.0"
}
},
"pug-error": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.0.0.tgz",
"integrity": "sha512-sjiUsi9M4RAGHktC1drQfCr5C5eriu24Lfbt4s+7SykztEOwVZtbFk1RRq0tzLxcMxMYTBR+zMQaG07J/btayQ==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.1.0.tgz",
"integrity": "sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==",
"dev": true,
"optional": true
},
@ -7412,8 +7438,7 @@
"to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
"dev": true
"integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog=="
},
"to-regex-range": {
"version": "5.0.1",
@ -7500,11 +7525,11 @@
"optional": true
},
"vue": {
"version": "2.7.14",
"resolved": "https://registry.npmjs.org/vue/-/vue-2.7.14.tgz",
"integrity": "sha512-b2qkFyOM0kwqWFuQmgd4o+uHGU7T+2z3T+WQp8UBjADfEv2n4FEMffzBmCKNP0IGzOEEfYjvtcC62xaSKeQDrQ==",
"version": "2.7.16",
"resolved": "https://registry.npmjs.org/vue/-/vue-2.7.16.tgz",
"integrity": "sha512-4gCtFXaAA3zYZdTp5s4Hl2sozuySsgz4jy1EnpBHNfpMa9dK1ZCG7viqBPCwXtmgc8nHqUsAu3G4gtmXkkY3Sw==",
"requires": {
"@vue/compiler-sfc": "2.7.14",
"@vue/compiler-sfc": "2.7.16",
"csstype": "^3.1.0"
}
},
@ -7542,9 +7567,9 @@
"integrity": "sha512-VYXZQLtjuvKxxcshuRAwjHnciqZVoXAjTjcqBTz4rKc8qih9g9pI3hbDjmqXaHdgL3v8pV6P8Z335XvHzESxLQ=="
},
"vue-template-compiler": {
"version": "2.7.14",
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz",
"integrity": "sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==",
"version": "2.7.16",
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz",
"integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==",
"dev": true,
"requires": {
"de-indent": "^1.0.2",

View file

@ -18,7 +18,7 @@
"bootswatch": "^4.6.2",
"codejar": "^3.7.0",
"prismjs": "^1.29.0",
"vue": "^2.7.14",
"vue": "^2.7.16",
"vue-router": "^3.6.5"
}
}
}

View file

@ -78,12 +78,9 @@ func (c connector) StoreEncryptedCoreMeta(key string, value any) error {
}
func (c connector) ValidateEncryption() error {
validationHasher := sha512.New()
fmt.Fprint(validationHasher, c.encryptionSecret)
var (
storedHash string
validationHash = fmt.Sprintf("%x", validationHasher.Sum(nil))
validationHash = fmt.Sprintf("%x", sha512.Sum512([]byte(c.encryptionSecret)))
)
err := backoff.NewBackoff().

View file

@ -21,10 +21,10 @@ func NewLogrusLogWriterWithLevel(logger *logrus.Logger, level logrus.Level, dbDr
// Print implements the gorm.Logger interface
func (l LogWriter) Print(a ...any) {
fmt.Fprint(l.Writer, a...)
fmt.Fprint(l.Writer, a...) //nolint:errcheck // Interface ignores this error
}
// Printf implements the gorm.Logger interface
func (l LogWriter) Printf(format string, a ...any) {
fmt.Fprintf(l.Writer, format, a...)
fmt.Fprintf(l.Writer, format, a...) //nolint:errcheck // Interface ignores this error
}

View file

@ -45,7 +45,7 @@ func ParseBadgeLevels(m *irc.Message) BadgeCollection {
badges := strings.Split(badgeString, ",")
for _, b := range badges {
badgeParts := strings.Split(b, "/")
if len(badgeParts) != 2 { //nolint:gomnd // This is not a magic number but just an expected count
if len(badgeParts) != 2 { //nolint:mnd // This is not a magic number but just an expected count
continue
}

View file

@ -58,7 +58,7 @@ func (c *Client) BanUser(ctx context.Context, channel, username string, duration
return errors.Wrap(err, "encoding payload")
}
return errors.Wrap(
return errors.Wrapf(
c.Request(ctx, ClientRequestOpts{
AuthType: AuthTypeBearerToken,
Method: http.MethodPost,
@ -89,7 +89,7 @@ func (c *Client) BanUser(ctx context.Context, channel, username string, duration
return ValidateStatus(opts, resp)
},
}),
"executing ban request",
"executing ban request for %q in %q", username, channel,
)
}

View file

@ -36,7 +36,7 @@ func (c *Client) SearchCategories(ctx context.Context, name string) ([]Category,
for {
if err := c.Request(ctx, ClientRequestOpts{
AuthType: AuthTypeBearerToken,
AuthType: AuthTypeAppAccessToken,
Method: http.MethodGet,
OKStatus: http.StatusOK,
Out: &resp,

View file

@ -1,7 +1,9 @@
package twitch
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
@ -27,12 +29,59 @@ type (
TagIds []string `json:"tag_ids"` //revive:disable-line:var-naming // Disabled to prevent breaking change
IsMature bool `json:"is_mature"`
}
// StreamMarkerInfo contains information about a marker on a stream
StreamMarkerInfo struct {
ID string `json:"id"`
CreatedAt time.Time `json:"created_at"`
Description string `json:"description"`
PositionSeconds int64 `json:"position_seconds"`
}
)
// ErrNoStreamsFound allows to differntiate between an HTTP error and
// the fact there just is no stream found
var ErrNoStreamsFound = errors.New("no streams found")
// CreateStreamMarker creates a marker for the currently running stream.
// The stream must be live, no VoD, no upload and no re-run.
// The description may be up to 140 chars and can be omitted.
func (c *Client) CreateStreamMarker(ctx context.Context, description string) (marker StreamMarkerInfo, err error) {
body := new(bytes.Buffer)
userID, _, err := c.GetAuthorizedUser(ctx)
if err != nil {
return marker, fmt.Errorf("getting ID for current user: %w", err)
}
if err = json.NewEncoder(body).Encode(struct {
UserID string `json:"user_id"`
Description string `json:"description,omitempty"`
}{
UserID: userID,
Description: description,
}); err != nil {
return marker, fmt.Errorf("encoding payload: %w", err)
}
var payload struct {
Data []StreamMarkerInfo `json:"data"`
}
if err := c.Request(ctx, ClientRequestOpts{
AuthType: AuthTypeBearerToken,
Body: body,
Method: http.MethodPost,
OKStatus: http.StatusOK,
Out: &payload,
URL: "https://api.twitch.tv/helix/streams/markers",
}); err != nil {
return marker, fmt.Errorf("creating marker: %w", err)
}
return payload.Data[0], nil
}
// GetCurrentStreamInfo returns the StreamInfo of the currently running
// stream of the given username
func (c *Client) GetCurrentStreamInfo(ctx context.Context, username string) (*StreamInfo, error) {

150
pkg/twitch/videos.go Normal file
View file

@ -0,0 +1,150 @@
package twitch
import (
"context"
"fmt"
"net/http"
"net/url"
"strconv"
"time"
"github.com/mitchellh/hashstructure/v2"
)
type (
// GetVideoOpts contain the query parameter for the GetVideos query
//
// See https://dev.twitch.tv/docs/api/reference/#get-videos for details
GetVideoOpts struct {
ID string // Required: Exactly one of ID, UserID, GameID
UserID string // Required: Exactly one of ID, UserID, GameID
GameID string // Required: Exactly one of ID, UserID, GameID
Language string // Optional: Use only with GameID
Period GetVideoOptsPeriod // Optional: Use only with GameID or UserID
Sort GetVideoOptsSort // Optional: Use only with GameID or UserID
Type GetVideoOptsType // Optional: Use only with GameID or UserID
First int64 // Optional: Use only with GameID or UserID
After string // Optional: Use only with UserID
Before string // Optional: Use only with UserID
}
// GetVideoOptsPeriod represents a filter used to filter the list of
// videos by when they were published
GetVideoOptsPeriod string
// GetVideoOptsSort represents the order to sort the returned videos in
GetVideoOptsSort string
// GetVideoOptsType represents a filter used to filter the list of
// videos by the video's type
GetVideoOptsType string
// Video contains information about a published video
Video struct {
ID string `json:"id"`
StreamID *string `json:"stream_id"`
UserID string `json:"user_id"`
UserLogin string `json:"user_login"`
UserName string `json:"user_name"`
Title string `json:"title"`
Description string `json:"description"`
CreatedAt time.Time `json:"created_at"`
PublishedAt time.Time `json:"published_at"`
URL string `json:"url"`
ThumbnailURL string `json:"thumbnail_url"`
Viewable string `json:"viewable"`
ViewCount int64 `json:"view_count"`
Language string `json:"language"`
Type string `json:"type"`
Duration string `json:"duration"`
MutedSegments []struct {
Duration int64 `json:"duration"`
Offset int64 `json:"offset"`
} `json:"muted_segments"`
}
)
// List of filters for GetVideoOpts.Period
const (
GetVideoOptsPeriodAll GetVideoOptsPeriod = "all"
GetVideoOptsPeriodDay GetVideoOptsPeriod = "day"
GetVideoOptsPeriodMonth GetVideoOptsPeriod = "month"
GetVideoOptsPeriodWeek GetVideoOptsPeriod = "week"
)
// List of sort options for GetVideoOpts.Sort
const (
GetVideoOptsSortTime GetVideoOptsSort = "time"
GetVideoOptsSortTrending GetVideoOptsSort = "trending"
GetVideoOptsSortViews GetVideoOptsSort = "views"
)
// List of types for GetVideoOpts.Type
const (
GetVideoOptsTypeAll GetVideoOptsType = "all"
GetVideoOptsTypeArchive GetVideoOptsType = "archive"
GetVideoOptsTypeHighlight GetVideoOptsType = "highlight"
GetVideoOptsTypeUpload GetVideoOptsType = "upload"
)
// GetVideos fetches information about one or more published videos
func (c *Client) GetVideos(ctx context.Context, opts GetVideoOpts) (videos []Video, err error) {
optsCacheKey, err := opts.cacheKey()
if err != nil {
return nil, fmt.Errorf("getting opts cache key: %w", err)
}
cacheKey := []string{"currentVideos", optsCacheKey}
if vids := c.apiCache.Get(cacheKey); vids != nil {
return vids.([]Video), nil
}
var payload struct {
Data []Video `json:"data"`
}
if err := c.Request(ctx, ClientRequestOpts{
AuthType: AuthTypeAppAccessToken,
Method: http.MethodGet,
OKStatus: http.StatusOK,
Out: &payload,
URL: fmt.Sprintf("https://api.twitch.tv/helix/videos?%s", opts.queryParams()),
}); err != nil {
return nil, fmt.Errorf("requesting videos: %w", err)
}
// Videos can be changed at any moment, cache for a short period of time
c.apiCache.Set(cacheKey, twitchMinCacheTime, payload.Data)
return payload.Data, nil
}
func (g GetVideoOpts) cacheKey() (string, error) {
h, err := hashstructure.Hash(g, hashstructure.FormatV2, nil)
if err != nil {
return "", fmt.Errorf("hashing opts: %w", err)
}
return strconv.FormatUint(h, 10), nil
}
func (g GetVideoOpts) queryParams() string {
params := url.Values{}
for k, v := range map[string]string{
"id": g.ID,
"user_id": g.UserID,
"game_id": g.GameID,
"language": g.Language,
"period": string(g.Period),
"sort": string(g.Sort),
"type": string(g.Type),
"first": strconv.FormatInt(g.First, 10),
"after": g.After,
"before": g.Before,
} {
if v != "" && v != "0" {
params.Set(k, v)
}
}
return params.Encode()
}

View file

@ -52,9 +52,7 @@ func (t *testTimerStore) InCooldown(tt TimerType, limiter, ruleID string) (bool,
}
func (testTimerStore) getCooldownTimerKey(tt TimerType, limiter, ruleID string) string {
h := sha256.New()
fmt.Fprintf(h, "%d:%s:%s", tt, limiter, ruleID)
return fmt.Sprintf("sha256:%x", h.Sum(nil))
return fmt.Sprintf("sha256:%x", sha256.Sum256([]byte(fmt.Sprintf("%d:%s:%s", tt, limiter, ruleID))))
}
// Permit timer
@ -69,7 +67,5 @@ func (t *testTimerStore) HasPermit(channel, username string) (bool, error) {
}
func (testTimerStore) getPermitTimerKey(channel, username string) string {
h := sha256.New()
fmt.Fprintf(h, "%d:%s:%s", TimerTypePermit, channel, strings.ToLower(strings.TrimLeft(username, "@")))
return fmt.Sprintf("sha256:%x", h.Sum(nil))
return fmt.Sprintf("sha256:%x", sha256.Sum256([]byte(fmt.Sprintf("%d:%s:%s", TimerTypePermit, channel, strings.ToLower(strings.TrimLeft(username, "@"))))))
}

View file

@ -25,6 +25,7 @@ import (
"github.com/Luzifer/twitch-bot/v3/internal/actors/linkdetector"
"github.com/Luzifer/twitch-bot/v3/internal/actors/linkprotect"
logActor "github.com/Luzifer/twitch-bot/v3/internal/actors/log"
"github.com/Luzifer/twitch-bot/v3/internal/actors/marker"
"github.com/Luzifer/twitch-bot/v3/internal/actors/messagehook"
"github.com/Luzifer/twitch-bot/v3/internal/actors/modchannel"
"github.com/Luzifer/twitch-bot/v3/internal/actors/nuke"
@ -78,6 +79,7 @@ var (
linkdetector.Register,
linkprotect.Register,
logActor.Register,
marker.Register,
messagehook.Register,
modchannel.Register,
nuke.Register,

View file

@ -5,7 +5,7 @@ import "github.com/Luzifer/twitch-bot/v3/pkg/twitch"
var (
channelExtendedScopes = map[string]string{
twitch.ScopeChannelEditCommercial: "run commercial",
twitch.ScopeChannelManageBroadcast: "modify category / title",
twitch.ScopeChannelManageBroadcast: "modify category / title, create markers",
twitch.ScopeChannelManagePolls: "manage polls",
twitch.ScopeChannelManagePredictions: "manage predictions",
twitch.ScopeChannelManageRaids: "start raids",

View file

@ -1127,7 +1127,7 @@ export default {
},
validateRaffleChannel() {
if (!/^[a-zA-Z0-9]{4,25}$/.test(this.models.raffle.channel)) {
if (!constants.REGEXP_USER.test(this.models.raffle.channel)) {
return false
}
return null

194
tools/go.mod Normal file
View file

@ -0,0 +1,194 @@
module tools
go 1.23.1
require (
github.com/golangci/golangci-lint v1.61.0
gotest.tools/gotestsum v1.12.0
mvdan.cc/gofumpt v0.7.0
)
require (
4d63.com/gocheckcompilerdirectives v1.2.1 // indirect
4d63.com/gochecknoglobals v0.2.1 // indirect
github.com/4meepo/tagalign v1.3.4 // indirect
github.com/Abirdcfly/dupword v0.1.1 // indirect
github.com/Antonboom/errname v0.1.13 // indirect
github.com/Antonboom/nilnil v0.1.9 // indirect
github.com/Antonboom/testifylint v1.4.3 // indirect
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect
github.com/Crocmagnon/fatcontext v0.5.2 // indirect
github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect
github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.0 // indirect
github.com/Masterminds/semver/v3 v3.3.0 // indirect
github.com/OpenPeeDeeP/depguard/v2 v2.2.0 // indirect
github.com/alecthomas/go-check-sumtype v0.1.4 // indirect
github.com/alexkohler/nakedret/v2 v2.0.4 // indirect
github.com/alexkohler/prealloc v1.0.0 // indirect
github.com/alingse/asasalint v0.0.11 // indirect
github.com/ashanbrown/forbidigo v1.6.0 // indirect
github.com/ashanbrown/makezero v1.1.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bitfield/gotestdox v0.2.2 // indirect
github.com/bkielbasa/cyclop v1.2.1 // indirect
github.com/blizzy78/varnamelen v0.8.0 // indirect
github.com/bombsimon/wsl/v4 v4.4.1 // indirect
github.com/breml/bidichk v0.2.7 // indirect
github.com/breml/errchkjson v0.3.6 // indirect
github.com/butuzov/ireturn v0.3.0 // indirect
github.com/butuzov/mirror v1.2.0 // indirect
github.com/catenacyber/perfsprint v0.7.1 // indirect
github.com/ccojocar/zxcvbn-go v1.0.2 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/charithe/durationcheck v0.0.10 // indirect
github.com/chavacava/garif v0.1.0 // indirect
github.com/ckaznocha/intrange v0.2.0 // indirect
github.com/curioswitch/go-reassign v0.2.0 // indirect
github.com/daixiang0/gci v0.13.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/denis-tingaikin/go-header v0.5.0 // indirect
github.com/dnephin/pflag v1.0.7 // indirect
github.com/ettle/strcase v0.2.0 // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/fatih/structtag v1.2.0 // indirect
github.com/firefart/nonamedreturns v1.0.5 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/fzipp/gocyclo v0.6.0 // indirect
github.com/ghostiam/protogetter v0.3.6 // indirect
github.com/go-critic/go-critic v0.11.4 // indirect
github.com/go-toolsmith/astcast v1.1.0 // indirect
github.com/go-toolsmith/astcopy v1.1.0 // indirect
github.com/go-toolsmith/astequal v1.2.0 // indirect
github.com/go-toolsmith/astfmt v1.1.0 // indirect
github.com/go-toolsmith/astp v1.1.0 // indirect
github.com/go-toolsmith/strparse v1.1.0 // indirect
github.com/go-toolsmith/typep v1.1.0 // indirect
github.com/go-viper/mapstructure/v2 v2.1.0 // indirect
github.com/go-xmlfmt/xmlfmt v1.1.2 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gofrs/flock v0.12.1 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a // indirect
github.com/golangci/gofmt v0.0.0-20240816233607-d8596aa466a9 // indirect
github.com/golangci/misspell v0.6.0 // indirect
github.com/golangci/modinfo v0.3.4 // indirect
github.com/golangci/plugin-module-register v0.1.1 // indirect
github.com/golangci/revgrep v0.5.3 // indirect
github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/gordonklaus/ineffassign v0.1.0 // indirect
github.com/gostaticanalysis/analysisutil v0.7.1 // indirect
github.com/gostaticanalysis/comment v1.4.2 // indirect
github.com/gostaticanalysis/forcetypeassert v0.1.0 // indirect
github.com/gostaticanalysis/nilerr v0.1.1 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hexops/gotextdiff v1.0.3 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jgautheron/goconst v1.7.1 // indirect
github.com/jingyugao/rowserrcheck v1.1.1 // indirect
github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af // indirect
github.com/jjti/go-spancheck v0.6.2 // indirect
github.com/julz/importas v0.1.0 // indirect
github.com/karamaru-alpha/copyloopvar v1.1.0 // indirect
github.com/kisielk/errcheck v1.7.0 // indirect
github.com/kkHAIKE/contextcheck v1.1.5 // indirect
github.com/kulti/thelper v0.6.3 // indirect
github.com/kunwardeep/paralleltest v1.0.10 // indirect
github.com/kyoh86/exportloopref v0.1.11 // indirect
github.com/lasiar/canonicalheader v1.1.1 // indirect
github.com/ldez/gomoddirectives v0.2.4 // indirect
github.com/ldez/tagliatelle v0.5.0 // indirect
github.com/leonklingele/grouper v1.1.2 // indirect
github.com/lufeee/execinquery v1.2.1 // indirect
github.com/macabu/inamedparam v0.1.3 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/maratori/testableexamples v1.0.0 // indirect
github.com/maratori/testpackage v1.1.1 // indirect
github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mgechev/revive v1.3.9 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moricho/tparallel v0.3.2 // indirect
github.com/nakabonne/nestif v0.3.1 // indirect
github.com/nishanths/exhaustive v0.12.0 // indirect
github.com/nishanths/predeclared v0.2.2 // indirect
github.com/nunnatsa/ginkgolinter v0.16.2 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/polyfloyd/go-errorlint v1.6.0 // indirect
github.com/prometheus/client_golang v1.12.1 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 // indirect
github.com/quasilyte/go-ruleguard/dsl v0.3.22 // indirect
github.com/quasilyte/gogrep v0.5.0 // indirect
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect
github.com/ryancurrah/gomodguard v1.3.5 // indirect
github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect
github.com/sanposhiho/wastedassign/v2 v2.0.7 // indirect
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect
github.com/sashamelentyev/interfacebloat v1.1.0 // indirect
github.com/sashamelentyev/usestdlibvars v1.27.0 // indirect
github.com/securego/gosec/v2 v2.21.2 // indirect
github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sivchari/containedctx v1.0.3 // indirect
github.com/sivchari/tenv v1.10.0 // indirect
github.com/sonatard/noctx v0.0.2 // indirect
github.com/sourcegraph/go-diff v0.7.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/cobra v1.8.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.12.0 // indirect
github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect
github.com/stbenjam/no-sprintf-host-port v0.1.1 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/subosito/gotenv v1.4.1 // indirect
github.com/tdakkota/asciicheck v0.2.0 // indirect
github.com/tetafro/godot v1.4.17 // indirect
github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966 // indirect
github.com/timonwong/loggercheck v0.9.4 // indirect
github.com/tomarrell/wrapcheck/v2 v2.9.0 // indirect
github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect
github.com/ultraware/funlen v0.1.0 // indirect
github.com/ultraware/whitespace v0.1.1 // indirect
github.com/uudashr/gocognit v1.1.3 // indirect
github.com/xen0n/gosmopolitan v1.2.2 // indirect
github.com/yagipy/maintidx v1.0.0 // indirect
github.com/yeya24/promlinter v0.3.0 // indirect
github.com/ykadowak/zerologlint v0.1.5 // indirect
gitlab.com/bosi/decorder v0.4.2 // indirect
go-simpler.org/musttag v0.12.2 // indirect
go-simpler.org/sloglint v0.7.2 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/automaxprocs v1.5.3 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.24.0 // indirect
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect
golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f // indirect
golang.org/x/mod v0.21.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/term v0.18.0 // indirect
golang.org/x/text v0.18.0 // indirect
golang.org/x/tools v0.24.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
honnef.co/go/tools v0.5.1 // indirect
mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f // indirect
)

1002
tools/go.sum Normal file

File diff suppressed because it is too large Load diff

9
tools/tools.go Normal file
View file

@ -0,0 +1,9 @@
//go:build tools
package tools
import (
_ "github.com/golangci/golangci-lint/cmd/golangci-lint"
_ "gotest.tools/gotestsum"
_ "mvdan.cc/gofumpt"
)

View file

@ -234,7 +234,6 @@ func (t *twitchWatcher) getTopicRegistrations(userID string) []topicRegistration
},
{
Topic: twitch.EventSubEventTypeChannelSuspiciousUserMessage,
Version: twitch.EventSubTopicVersionBeta,
Condition: twitch.EventSubCondition{BroadcasterUserID: userID, ModeratorUserID: userID},
RequiredScopes: []string{twitch.ScopeModeratorReadSuspiciousUsers},
Hook: t.handleEventSubSusUserMessage,
@ -242,7 +241,6 @@ func (t *twitchWatcher) getTopicRegistrations(userID string) []topicRegistration
},
{
Topic: twitch.EventSubEventTypeChannelSuspiciousUserUpdate,
Version: twitch.EventSubTopicVersionBeta,
Condition: twitch.EventSubCondition{BroadcasterUserID: userID, ModeratorUserID: userID},
RequiredScopes: []string{twitch.ScopeModeratorReadSuspiciousUsers},
Hook: t.handleEventSubSusUserUpdate,