mirror of
https://github.com/Luzifer/twitch-bot.git
synced 2024-11-08 16:20:02 +00:00
Compare commits
7 commits
8819b4031a
...
ff475f286b
Author | SHA1 | Date | |
---|---|---|---|
ff475f286b | |||
06d7fcb019 | |||
710783aaf7 | |||
19038dbc6e | |||
740a71a173 | |||
e0a8ce3684 | |||
5a8459cedc |
16 changed files with 488 additions and 104 deletions
|
@ -1,3 +1,12 @@
|
||||||
|
# 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
|
# 3.33.2 / 2024-08-27
|
||||||
|
|
||||||
* Bugfixes
|
* Bugfixes
|
||||||
|
|
7
Makefile
7
Makefile
|
@ -40,9 +40,10 @@ node_modules:
|
||||||
|
|
||||||
# --- Tools
|
# --- Tools
|
||||||
|
|
||||||
update_ua_list:
|
update-chrome-major:
|
||||||
# User-Agents provided by https://www.useragents.me/
|
sed -i -E \
|
||||||
curl -sSf https://www.useragents.me/api | jq -r '.data[].ua' | grep -v 'Trident' >internal/linkcheck/user-agents.txt
|
'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:
|
||||||
bash ci/create-workflow.sh
|
bash ci/create-workflow.sh
|
||||||
|
|
|
@ -84,6 +84,23 @@ Triggers the creation of a Clip from the given channel owned by the creator (sub
|
||||||
add_delay: false
|
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
|
## Custom Event
|
||||||
|
|
||||||
Create a custom event
|
Create a custom event
|
||||||
|
|
|
@ -165,6 +165,19 @@ Example:
|
||||||
* 1 6
|
* 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`
|
### `displayName`
|
||||||
|
|
||||||
Returns the display name the specified user set for themselves
|
Returns the display name the specified user set for themselves
|
||||||
|
@ -467,7 +480,7 @@ Example:
|
||||||
|
|
||||||
```
|
```
|
||||||
# Your int this hour: {{ printf "%.0f" (mulf (seededRandom (list "int" .username (now | date "2006-01-02 15") | join ":")) 100) }}%
|
# Your int this hour: {{ printf "%.0f" (mulf (seededRandom (list "int" .username (now | date "2006-01-02 15") | join ":")) 100) }}%
|
||||||
< Your int this hour: 41%
|
< Your int this hour: 23%
|
||||||
```
|
```
|
||||||
|
|
||||||
### `spotifyCurrentPlaying`
|
### `spotifyCurrentPlaying`
|
||||||
|
|
|
@ -116,7 +116,7 @@ SocketMessage received for every event and passed to the new `(eventObj) => { ..
|
||||||
|
|
||||||
| Name | Type | Description |
|
| 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) |
|
| [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`) |
|
| [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 |
|
| [time] | <code>String</code> | RFC3339 timestamp of the event |
|
||||||
|
|
114
internal/actors/marker/actor.go
Normal file
114
internal/actors/marker/actor.go
Normal 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
|
||||||
|
}
|
|
@ -18,23 +18,23 @@ func testGenerateRaffe() raffle {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now lets generate 132 non-followers taking part
|
// Now lets generate 132 non-followers taking part
|
||||||
for i := 0; i < 132; i++ {
|
for i := uint64(0); i < 132; i++ {
|
||||||
r.Entries = append(r.Entries, raffleEntry{ID: uint64(i), Multiplier: 1})
|
r.Entries = append(r.Entries, raffleEntry{ID: i, Multiplier: 1})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now lets generate 500 followers taking part
|
// Now lets generate 500 followers taking part
|
||||||
for i := 0; i < 500; i++ {
|
for i := uint64(0); i < 500; i++ {
|
||||||
r.Entries = append(r.Entries, raffleEntry{ID: 10000 + uint64(i), Multiplier: r.MultiFollower})
|
r.Entries = append(r.Entries, raffleEntry{ID: 10000 + i, Multiplier: r.MultiFollower})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now lets generate 200 subscribers taking part
|
// Now lets generate 200 subscribers taking part
|
||||||
for i := 0; i < 200; i++ {
|
for i := uint64(0); i < 200; i++ {
|
||||||
r.Entries = append(r.Entries, raffleEntry{ID: 20000 + uint64(i), Multiplier: r.MultiSubscriber})
|
r.Entries = append(r.Entries, raffleEntry{ID: 20000 + i, Multiplier: r.MultiSubscriber})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now lets generate 5 VIPs taking part
|
// Now lets generate 5 VIPs taking part
|
||||||
for i := 0; i < 5; i++ {
|
for i := uint64(0); i < 5; i++ {
|
||||||
r.Entries = append(r.Entries, raffleEntry{ID: 30000 + uint64(i), Multiplier: r.MultiVIP})
|
r.Entries = append(r.Entries, raffleEntry{ID: 30000 + i, Multiplier: r.MultiVIP})
|
||||||
}
|
}
|
||||||
|
|
||||||
// They didn't join in order so lets shuffle them
|
// They didn't join in order so lets shuffle them
|
||||||
|
|
|
@ -57,13 +57,12 @@ func TestScanForLinks(t *testing.T) {
|
||||||
t.SkipNow()
|
t.SkipNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
c := New()
|
|
||||||
|
|
||||||
for _, testCase := range []struct {
|
for _, testCase := range []struct {
|
||||||
Heuristic bool
|
Heuristic bool
|
||||||
Message string
|
Message string
|
||||||
ExpectedLinks []string
|
ExpectedLinks []string
|
||||||
ExpectedContains bool
|
ExpectedContains bool
|
||||||
|
TraceStack bool
|
||||||
}{
|
}{
|
||||||
// Case: full URL is present in the message
|
// 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},
|
{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) {
|
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
|
var linksFound []string
|
||||||
if testCase.Heuristic {
|
if testCase.Heuristic {
|
||||||
linksFound = c.HeuristicScanForLinks(testCase.Message)
|
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,16 +2,14 @@ package linkcheck
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
|
||||||
_ "embed"
|
|
||||||
"io"
|
"io"
|
||||||
"math/big"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/cookiejar"
|
"net/http/cookiejar"
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
@ -30,6 +28,8 @@ type (
|
||||||
resolver struct {
|
resolver struct {
|
||||||
resolverC chan resolverQueueEntry
|
resolverC chan resolverQueueEntry
|
||||||
skipValidation bool
|
skipValidation bool
|
||||||
|
|
||||||
|
t *testing.T
|
||||||
}
|
}
|
||||||
|
|
||||||
resolverQueueEntry struct {
|
resolverQueueEntry struct {
|
||||||
|
@ -40,20 +40,12 @@ type (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
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]`)
|
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]+)?$`)
|
numericHost = regexp.MustCompile(`^(?:[0-9]+\.)*[0-9]+(?::[0-9]+)?$`)
|
||||||
|
|
||||||
//go:embed user-agents.txt
|
|
||||||
uaList string
|
|
||||||
|
|
||||||
defaultResolver = newResolver(resolverPoolSize)
|
defaultResolver = newResolver(resolverPoolSize)
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
|
||||||
defaultUserAgents = strings.Split(strings.TrimSpace(uaList), "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func newResolver(poolSize int, opts ...func(*resolver)) *resolver {
|
func newResolver(poolSize int, opts ...func(*resolver)) *resolver {
|
||||||
r := &resolver{
|
r := &resolver{
|
||||||
resolverC: make(chan resolverQueueEntry),
|
resolverC: make(chan resolverQueueEntry),
|
||||||
|
@ -74,6 +66,10 @@ func withSkipVerify() func(*resolver) {
|
||||||
return func(r *resolver) { r.skipValidation = true }
|
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) {
|
func (r resolver) Resolve(qe resolverQueueEntry) {
|
||||||
qe.WaitGroup.Add(1)
|
qe.WaitGroup.Add(1)
|
||||||
r.resolverC <- qe
|
r.resolverC <- qe
|
||||||
|
@ -87,8 +83,8 @@ func (resolver) getJar() *cookiejar.Jar {
|
||||||
// resolveFinal takes a link and looks up the final destination of
|
// resolveFinal takes a link and looks up the final destination of
|
||||||
// that link after all redirects were followed
|
// that link after all redirects were followed
|
||||||
//
|
//
|
||||||
//nolint:gocyclo
|
//nolint:funlen,gocyclo
|
||||||
func (r resolver) resolveFinal(link string, cookieJar *cookiejar.Jar, callStack *stack, userAgent string) string {
|
func (r resolver) resolveFinal(link string, cookieJar *cookiejar.Jar, callStack *stack) string {
|
||||||
if !linkTest.MatchString(link) && !r.skipValidation {
|
if !linkTest.MatchString(link) && !r.skipValidation {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
@ -131,12 +127,19 @@ func (r resolver) resolveFinal(link string, cookieJar *cookiejar.Jar, callStack
|
||||||
// Sanitize host: Trailing dots are valid but not required
|
// Sanitize host: Trailing dots are valid but not required
|
||||||
u.Host = strings.TrimRight(u.Host, ".")
|
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)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Set("User-Agent", userAgent)
|
for k, v := range generateUserAgentHeaders() {
|
||||||
|
req.Header.Set(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -156,7 +159,7 @@ func (r resolver) resolveFinal(link string, cookieJar *cookiejar.Jar, callStack
|
||||||
}
|
}
|
||||||
target := r.resolveReference(u, tu)
|
target := r.resolveReference(u, tu)
|
||||||
callStack.Visit(link)
|
callStack.Visit(link)
|
||||||
return r.resolveFinal(target, cookieJar, callStack, userAgent)
|
return r.resolveFinal(target, cookieJar, callStack)
|
||||||
}
|
}
|
||||||
|
|
||||||
// We got a response, it's no redirect, lets check for in-document stuff
|
// We got a response, it's no redirect, lets check for in-document stuff
|
||||||
|
@ -173,14 +176,14 @@ func (r resolver) resolveFinal(link string, cookieJar *cookiejar.Jar, callStack
|
||||||
}
|
}
|
||||||
target := r.resolveReference(u, tu)
|
target := r.resolveReference(u, tu)
|
||||||
callStack.Visit(link)
|
callStack.Visit(link)
|
||||||
return r.resolveFinal(target, cookieJar, callStack, userAgent)
|
return r.resolveFinal(target, cookieJar, callStack)
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.Header.Get("Set-Cookie") != "" {
|
if resp.Header.Get("Set-Cookie") != "" {
|
||||||
// A new cookie was set, lets refresh the page once to see if stuff
|
// A new cookie was set, lets refresh the page once to see if stuff
|
||||||
// changes with that new cookie
|
// changes with that new cookie
|
||||||
callStack.Visit(link)
|
callStack.Visit(link)
|
||||||
return r.resolveFinal(u.String(), cookieJar, callStack, userAgent)
|
return r.resolveFinal(u.String(), cookieJar, callStack)
|
||||||
}
|
}
|
||||||
|
|
||||||
// We had no in-document redirects: we count this as a success
|
// We had no in-document redirects: we count this as a success
|
||||||
|
@ -226,14 +229,9 @@ func (resolver) resolveReference(origin *url.URL, loc *url.URL) string {
|
||||||
|
|
||||||
func (r resolver) runResolver() {
|
func (r resolver) runResolver() {
|
||||||
for qe := range r.resolverC {
|
for qe := range r.resolverC {
|
||||||
if link := r.resolveFinal(qe.Link, r.getJar(), &stack{}, r.userAgent()); link != "" {
|
if link := r.resolveFinal(qe.Link, r.getJar(), &stack{}); link != "" {
|
||||||
qe.Callback(link)
|
qe.Callback(link)
|
||||||
}
|
}
|
||||||
qe.WaitGroup.Done()
|
qe.WaitGroup.Done()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (resolver) userAgent() string {
|
|
||||||
n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(defaultUserAgents))))
|
|
||||||
return defaultUserAgents[n.Int64()]
|
|
||||||
}
|
|
||||||
|
|
|
@ -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
|
|
38
internal/linkcheck/useragent.go
Normal file
38
internal/linkcheck/useragent.go
Normal 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/template/twitch/videos.go
Normal file
50
internal/template/twitch/videos.go
Normal 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",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,7 +1,9 @@
|
||||||
package twitch
|
package twitch
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
@ -27,12 +29,59 @@ type (
|
||||||
TagIds []string `json:"tag_ids"` //revive:disable-line:var-naming // Disabled to prevent breaking change
|
TagIds []string `json:"tag_ids"` //revive:disable-line:var-naming // Disabled to prevent breaking change
|
||||||
IsMature bool `json:"is_mature"`
|
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
|
// ErrNoStreamsFound allows to differntiate between an HTTP error and
|
||||||
// the fact there just is no stream found
|
// the fact there just is no stream found
|
||||||
var ErrNoStreamsFound = errors.New("no streams 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
|
// GetCurrentStreamInfo returns the StreamInfo of the currently running
|
||||||
// stream of the given username
|
// stream of the given username
|
||||||
func (c *Client) GetCurrentStreamInfo(ctx context.Context, username string) (*StreamInfo, error) {
|
func (c *Client) GetCurrentStreamInfo(ctx context.Context, username string) (*StreamInfo, error) {
|
||||||
|
|
150
pkg/twitch/videos.go
Normal file
150
pkg/twitch/videos.go
Normal 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()
|
||||||
|
}
|
|
@ -25,6 +25,7 @@ import (
|
||||||
"github.com/Luzifer/twitch-bot/v3/internal/actors/linkdetector"
|
"github.com/Luzifer/twitch-bot/v3/internal/actors/linkdetector"
|
||||||
"github.com/Luzifer/twitch-bot/v3/internal/actors/linkprotect"
|
"github.com/Luzifer/twitch-bot/v3/internal/actors/linkprotect"
|
||||||
logActor "github.com/Luzifer/twitch-bot/v3/internal/actors/log"
|
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/messagehook"
|
||||||
"github.com/Luzifer/twitch-bot/v3/internal/actors/modchannel"
|
"github.com/Luzifer/twitch-bot/v3/internal/actors/modchannel"
|
||||||
"github.com/Luzifer/twitch-bot/v3/internal/actors/nuke"
|
"github.com/Luzifer/twitch-bot/v3/internal/actors/nuke"
|
||||||
|
@ -78,6 +79,7 @@ var (
|
||||||
linkdetector.Register,
|
linkdetector.Register,
|
||||||
linkprotect.Register,
|
linkprotect.Register,
|
||||||
logActor.Register,
|
logActor.Register,
|
||||||
|
marker.Register,
|
||||||
messagehook.Register,
|
messagehook.Register,
|
||||||
modchannel.Register,
|
modchannel.Register,
|
||||||
nuke.Register,
|
nuke.Register,
|
||||||
|
|
|
@ -5,7 +5,7 @@ import "github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
||||||
var (
|
var (
|
||||||
channelExtendedScopes = map[string]string{
|
channelExtendedScopes = map[string]string{
|
||||||
twitch.ScopeChannelEditCommercial: "run commercial",
|
twitch.ScopeChannelEditCommercial: "run commercial",
|
||||||
twitch.ScopeChannelManageBroadcast: "modify category / title",
|
twitch.ScopeChannelManageBroadcast: "modify category / title, create markers",
|
||||||
twitch.ScopeChannelManagePolls: "manage polls",
|
twitch.ScopeChannelManagePolls: "manage polls",
|
||||||
twitch.ScopeChannelManagePredictions: "manage predictions",
|
twitch.ScopeChannelManagePredictions: "manage predictions",
|
||||||
twitch.ScopeChannelManageRaids: "start raids",
|
twitch.ScopeChannelManageRaids: "start raids",
|
||||||
|
|
Loading…
Reference in a new issue