mirror of
https://github.com/Luzifer/twitch-bot.git
synced 2024-11-09 16:50:01 +00:00
[core] Extend API and replace deprecated chat commands (#34)
This commit is contained in:
parent
1409a4bd34
commit
064c7432ed
32 changed files with 1133 additions and 639 deletions
4
auth.go
4
auth.go
|
@ -79,7 +79,7 @@ func handleAuthUpdateBotToken(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
botUser, err := twitch.New(cfg.TwitchClient, cfg.TwitchClientSecret, rData.AccessToken, "").GetAuthorizedUsername()
|
||||
_, botUser, err := twitch.New(cfg.TwitchClient, cfg.TwitchClientSecret, rData.AccessToken, "").GetAuthorizedUser()
|
||||
if err != nil {
|
||||
http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
@ -135,7 +135,7 @@ func handleAuthUpdateChannelGrant(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
grantUser, err := twitch.New(cfg.TwitchClient, cfg.TwitchClientSecret, rData.AccessToken, "").GetAuthorizedUsername()
|
||||
_, grantUser, err := twitch.New(cfg.TwitchClient, cfg.TwitchClientSecret, rData.AccessToken, "").GetAuthorizedUser()
|
||||
if err != nil {
|
||||
http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
|
|
@ -151,7 +151,7 @@ func (a *autoMessage) Send(c *irc.Client) error {
|
|||
msg = fmt.Sprintf("\001ACTION %s\001", msg)
|
||||
}
|
||||
|
||||
if err := c.WriteMessage(&irc.Message{
|
||||
if err := sendMessage(&irc.Message{
|
||||
Command: "PRIVMSG",
|
||||
Params: []string{
|
||||
fmt.Sprintf("#%s", strings.TrimLeft(a.Channel, "#")),
|
||||
|
|
|
@ -17,7 +17,7 @@ func getAuthorizationFromRequest(r *http.Request) (string, *twitch.Client, error
|
|||
|
||||
tc := twitch.New(cfg.TwitchClient, cfg.TwitchClientSecret, token, "")
|
||||
|
||||
user, err := tc.GetAuthorizedUsername()
|
||||
_, user, err := tc.GetAuthorizedUser()
|
||||
return user, tc, errors.Wrap(err, "getting authorized user")
|
||||
}
|
||||
|
||||
|
|
47
chatcommands.go
Normal file
47
chatcommands.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/go-irc/irc"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/Luzifer/twitch-bot/v2/plugins"
|
||||
)
|
||||
|
||||
var (
|
||||
availableChatcommands = map[string]plugins.MsgModificationFunc{}
|
||||
availableChatcommandsLock = new(sync.RWMutex)
|
||||
)
|
||||
|
||||
func registerChatcommand(linePrefix string, modFn plugins.MsgModificationFunc) {
|
||||
availableChatcommandsLock.Lock()
|
||||
defer availableChatcommandsLock.Unlock()
|
||||
|
||||
if _, ok := availableChatcommands[linePrefix]; ok {
|
||||
log.WithField("linePrefix", linePrefix).Fatal("Duplicate registration of chatcommand")
|
||||
}
|
||||
|
||||
availableChatcommands[linePrefix] = modFn
|
||||
}
|
||||
|
||||
func handleChatcommandModifications(m *irc.Message) error {
|
||||
availableChatcommandsLock.RLock()
|
||||
defer availableChatcommandsLock.RUnlock()
|
||||
|
||||
msg := m.Trailing()
|
||||
|
||||
for prefix, modFn := range availableChatcommands {
|
||||
if !strings.HasPrefix(msg, prefix) {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := modFn(m); err != nil {
|
||||
return errors.Wrap(err, "modifying message")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -196,7 +196,7 @@ func configEditorHandleGeneralGet(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
var uName *string
|
||||
if n, err := twitchClient.GetAuthorizedUsername(); err == nil {
|
||||
if _, n, err := twitchClient.GetAuthorizedUser(); err == nil {
|
||||
uName = &n
|
||||
}
|
||||
|
||||
|
|
40
internal/actors/announce/actor.go
Normal file
40
internal/actors/announce/actor.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package announce
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/go-irc/irc"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/Luzifer/twitch-bot/v2/pkg/twitch"
|
||||
"github.com/Luzifer/twitch-bot/v2/plugins"
|
||||
)
|
||||
|
||||
var (
|
||||
botTwitchClient *twitch.Client
|
||||
|
||||
announceChatcommandRegex = regexp.MustCompile(`^/announce(|blue|green|orange|purple) +(.+)$`)
|
||||
)
|
||||
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
botTwitchClient = args.GetTwitchClient()
|
||||
|
||||
args.RegisterMessageModFunc("/announce", handleChatCommand)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleChatCommand(m *irc.Message) error {
|
||||
channel := plugins.DeriveChannel(m, nil)
|
||||
|
||||
matches := announceChatcommandRegex.FindStringSubmatch(m.Trailing())
|
||||
if matches == nil {
|
||||
return errors.New("announce message does not match required format")
|
||||
}
|
||||
|
||||
if err := botTwitchClient.SendChatAnnouncement(channel, matches[1], matches[2]); err != nil {
|
||||
return errors.Wrap(err, "sending announcement")
|
||||
}
|
||||
|
||||
return plugins.ErrSkipSendingMessage
|
||||
}
|
|
@ -2,25 +2,28 @@ package ban
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"regexp"
|
||||
|
||||
"github.com/go-irc/irc"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/Luzifer/twitch-bot/v2/pkg/twitch"
|
||||
"github.com/Luzifer/twitch-bot/v2/plugins"
|
||||
)
|
||||
|
||||
const actorName = "ban"
|
||||
|
||||
var (
|
||||
botTwitchClient *twitch.Client
|
||||
formatMessage plugins.MsgFormatter
|
||||
send plugins.SendMessageFunc
|
||||
|
||||
banChatcommandRegex = regexp.MustCompile(`^/ban +([^\s]+) +(.+)$`)
|
||||
)
|
||||
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
botTwitchClient = args.GetTwitchClient()
|
||||
formatMessage = args.FormatMessage
|
||||
send = args.SendMessage
|
||||
|
||||
args.RegisterActor(actorName, func() plugins.Actor { return &actor{} })
|
||||
|
||||
|
@ -35,7 +38,7 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
Description: "Reason why the user was banned",
|
||||
Key: "reason",
|
||||
Name: "Reason",
|
||||
Optional: true,
|
||||
Optional: false,
|
||||
SupportTemplate: true,
|
||||
Type: plugins.ActionDocumentationFieldTypeString,
|
||||
},
|
||||
|
@ -53,7 +56,7 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
{
|
||||
Description: "Reason to add to the ban",
|
||||
Name: "reason",
|
||||
Required: false,
|
||||
Required: true,
|
||||
Type: "string",
|
||||
},
|
||||
},
|
||||
|
@ -71,6 +74,8 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
},
|
||||
})
|
||||
|
||||
args.RegisterMessageModFunc("/ban", handleChatCommand)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -84,32 +89,14 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
|
|||
return false, errors.Wrap(err, "executing reason template")
|
||||
}
|
||||
|
||||
return a.execBan(
|
||||
return false, errors.Wrap(
|
||||
botTwitchClient.BanUser(
|
||||
plugins.DeriveChannel(m, eventData),
|
||||
plugins.DeriveUser(m, eventData),
|
||||
0,
|
||||
reason,
|
||||
)
|
||||
}
|
||||
|
||||
func (actor) execBan(channel, user, reason string) (bool, error) {
|
||||
cmd := []string{
|
||||
"/ban",
|
||||
user,
|
||||
}
|
||||
|
||||
if reason != "" {
|
||||
cmd = append(cmd, reason)
|
||||
}
|
||||
|
||||
return false, errors.Wrap(
|
||||
send(&irc.Message{
|
||||
Command: "PRIVMSG",
|
||||
Params: []string{
|
||||
"#" + strings.TrimLeft(channel, "#"),
|
||||
strings.Join(cmd, " "),
|
||||
},
|
||||
}),
|
||||
"sending ban",
|
||||
),
|
||||
"executing ban",
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -126,10 +113,25 @@ func handleAPIBan(w http.ResponseWriter, r *http.Request) {
|
|||
reason = r.FormValue("reason")
|
||||
)
|
||||
|
||||
if _, err := (actor{}).execBan(channel, user, reason); err != nil {
|
||||
if err := botTwitchClient.BanUser(channel, user, 0, reason); err != nil {
|
||||
http.Error(w, errors.Wrap(err, "issuing ban").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func handleChatCommand(m *irc.Message) error {
|
||||
channel := plugins.DeriveChannel(m, nil)
|
||||
|
||||
matches := banChatcommandRegex.FindStringSubmatch(m.Trailing())
|
||||
if matches == nil {
|
||||
return errors.New("ban message does not match required format")
|
||||
}
|
||||
|
||||
if err := botTwitchClient.BanUser(channel, matches[1], 0, matches[3]); err != nil {
|
||||
return errors.Wrap(err, "executing ban")
|
||||
}
|
||||
|
||||
return plugins.ErrSkipSendingMessage
|
||||
}
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
package deleteactor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-irc/irc"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/Luzifer/twitch-bot/v2/pkg/twitch"
|
||||
"github.com/Luzifer/twitch-bot/v2/plugins"
|
||||
)
|
||||
|
||||
const actorName = "delete"
|
||||
|
||||
var botTwitchClient *twitch.Client
|
||||
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
botTwitchClient = args.GetTwitchClient()
|
||||
|
||||
args.RegisterActor(actorName, func() plugins.Actor { return &actor{} })
|
||||
|
||||
args.RegisterActorDocumentation(plugins.ActionDocumentation{
|
||||
|
@ -32,14 +35,11 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
|
|||
}
|
||||
|
||||
return false, errors.Wrap(
|
||||
c.WriteMessage(&irc.Message{
|
||||
Command: "PRIVMSG",
|
||||
Params: []string{
|
||||
m.Params[0],
|
||||
fmt.Sprintf("/delete %s", msgID),
|
||||
},
|
||||
}),
|
||||
"sending delete",
|
||||
botTwitchClient.DeleteMessage(
|
||||
plugins.DeriveChannel(m, eventData),
|
||||
msgID,
|
||||
),
|
||||
"deleting message",
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -19,10 +19,14 @@ const (
|
|||
httpTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
var formatMessage plugins.MsgFormatter
|
||||
var (
|
||||
formatMessage plugins.MsgFormatter
|
||||
send plugins.SendMessageFunc
|
||||
)
|
||||
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
formatMessage = args.FormatMessage
|
||||
send = args.SendMessage
|
||||
|
||||
args.RegisterActor(actorName, func() plugins.Actor { return &actor{} })
|
||||
|
||||
|
@ -85,7 +89,7 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
|
|||
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
for scanner.Scan() {
|
||||
if err = c.WriteMessage(&irc.Message{
|
||||
if err = send(&irc.Message{
|
||||
Command: "PRIVMSG",
|
||||
Params: []string{
|
||||
plugins.DeriveChannel(m, eventData),
|
||||
|
|
48
internal/actors/nuke/actions.go
Normal file
48
internal/actors/nuke/actions.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
package nuke
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type (
|
||||
actionFn func(channel, match, msgid, user string) error
|
||||
)
|
||||
|
||||
func actionBan(channel, match, msgid, user string) error {
|
||||
return errors.Wrap(
|
||||
botTwitchClient.BanUser(
|
||||
channel,
|
||||
user,
|
||||
0,
|
||||
fmt.Sprintf("Nuke issued for %q", match),
|
||||
),
|
||||
"executing ban",
|
||||
)
|
||||
}
|
||||
|
||||
func actionDelete(channel, match, msgid, user string) (err error) {
|
||||
return errors.Wrap(
|
||||
botTwitchClient.DeleteMessage(
|
||||
channel,
|
||||
msgid,
|
||||
),
|
||||
"deleting message",
|
||||
)
|
||||
}
|
||||
|
||||
func getActionTimeout(duration time.Duration) actionFn {
|
||||
return func(channel, match, msgid, user string) error {
|
||||
return errors.Wrap(
|
||||
botTwitchClient.BanUser(
|
||||
channel,
|
||||
user,
|
||||
duration,
|
||||
fmt.Sprintf("Nuke issued for %q", match),
|
||||
),
|
||||
"executing timeout",
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
package nuke
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
@ -22,6 +21,7 @@ const (
|
|||
)
|
||||
|
||||
var (
|
||||
botTwitchClient *twitch.Client
|
||||
formatMessage plugins.MsgFormatter
|
||||
|
||||
messageStore = map[string][]*storedMessage{}
|
||||
|
@ -32,6 +32,7 @@ var (
|
|||
)
|
||||
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
botTwitchClient = args.GetTwitchClient()
|
||||
formatMessage = args.FormatMessage
|
||||
|
||||
args.RegisterActor(actorName, func() plugins.Actor { return &actor{} })
|
||||
|
@ -161,22 +162,28 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
|
|||
}
|
||||
scanTime := time.Now().Add(-scan)
|
||||
|
||||
var action string
|
||||
var (
|
||||
action actionFn
|
||||
actionName string
|
||||
)
|
||||
rawAction, err := formatMessage(attrs.MustString("action", ptrStringDelete), m, r, eventData)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "formatting action")
|
||||
}
|
||||
switch rawAction {
|
||||
case "delete":
|
||||
action = "/delete $msgid"
|
||||
action = actionDelete
|
||||
actionName = "delete $msgid"
|
||||
case "ban":
|
||||
action = `/ban $user Nuke issued for "$match"`
|
||||
action = actionBan
|
||||
actionName = "ban $user"
|
||||
default:
|
||||
to, err := time.ParseDuration(rawAction)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "parsing action duration")
|
||||
}
|
||||
action = fmt.Sprintf(`/timeout $user %d Nuke issued for "$match"`, to/time.Second)
|
||||
action = getActionTimeout(to)
|
||||
actionName = "timeout $user"
|
||||
}
|
||||
|
||||
channel := plugins.DeriveChannel(m, eventData)
|
||||
|
@ -202,23 +209,16 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
|
|||
}
|
||||
|
||||
enforcement := strings.NewReplacer(
|
||||
"$match", rawMatch,
|
||||
"$msgid", string(stMsg.Msg.Tags["id"]),
|
||||
"$user", plugins.DeriveUser(stMsg.Msg, nil),
|
||||
).Replace(action)
|
||||
).Replace(actionName)
|
||||
|
||||
if str.StringInSlice(enforcement, executedEnforcement) {
|
||||
continue
|
||||
}
|
||||
|
||||
if err = c.WriteMessage(&irc.Message{
|
||||
Command: "PRIVMSG",
|
||||
Params: []string{
|
||||
channel,
|
||||
enforcement,
|
||||
},
|
||||
}); err != nil {
|
||||
return false, errors.Wrap(err, "sending action")
|
||||
if err = action(channel, rawMatch, string(stMsg.Msg.Tags["id"]), plugins.DeriveUser(stMsg.Msg, nil)); err != nil {
|
||||
return false, errors.Wrap(err, "executing action")
|
||||
}
|
||||
|
||||
executedEnforcement = append(executedEnforcement, enforcement)
|
||||
|
|
|
@ -2,7 +2,6 @@ package punish
|
|||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -10,6 +9,7 @@ import (
|
|||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/Luzifer/twitch-bot/v2/pkg/database"
|
||||
"github.com/Luzifer/twitch-bot/v2/pkg/twitch"
|
||||
"github.com/Luzifer/twitch-bot/v2/plugins"
|
||||
)
|
||||
|
||||
|
@ -22,6 +22,7 @@ const (
|
|||
)
|
||||
|
||||
var (
|
||||
botTwitchClient *twitch.Client
|
||||
db database.Connector
|
||||
formatMessage plugins.MsgFormatter
|
||||
ptrDefaultCooldown = func(v time.Duration) *time.Duration { return &v }(oneWeek)
|
||||
|
@ -34,6 +35,7 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
return errors.Wrap(err, "applying schema migration")
|
||||
}
|
||||
|
||||
botTwitchClient = args.GetTwitchClient()
|
||||
formatMessage = args.FormatMessage
|
||||
|
||||
args.RegisterActor(actorNamePunish, func() plugins.Actor { return &actorPunish{} })
|
||||
|
@ -159,13 +161,15 @@ func (a actorPunish) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eve
|
|||
}
|
||||
nLvl := int(math.Min(float64(len(levels)-1), float64(lvl.LastLevel+1)))
|
||||
|
||||
var cmd []string
|
||||
|
||||
switch lt := levels[nLvl]; lt {
|
||||
case "ban":
|
||||
cmd = []string{"/ban", strings.TrimLeft(user, "@")}
|
||||
if reason != "" {
|
||||
cmd = append(cmd, reason)
|
||||
if err = botTwitchClient.BanUser(
|
||||
plugins.DeriveChannel(m, eventData),
|
||||
strings.TrimLeft(user, "@"),
|
||||
0,
|
||||
reason,
|
||||
); err != nil {
|
||||
return false, errors.Wrap(err, "executing user ban")
|
||||
}
|
||||
|
||||
case "delete":
|
||||
|
@ -174,7 +178,12 @@ func (a actorPunish) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eve
|
|||
return false, errors.New("found no mesage id")
|
||||
}
|
||||
|
||||
cmd = []string{"/delete", msgID}
|
||||
if err = botTwitchClient.DeleteMessage(
|
||||
plugins.DeriveChannel(m, eventData),
|
||||
msgID,
|
||||
); err != nil {
|
||||
return false, errors.Wrap(err, "deleting message")
|
||||
}
|
||||
|
||||
default:
|
||||
to, err := time.ParseDuration(lt)
|
||||
|
@ -182,20 +191,14 @@ func (a actorPunish) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eve
|
|||
return false, errors.Wrap(err, "parsing punishment level")
|
||||
}
|
||||
|
||||
cmd = []string{"/timeout", strings.TrimLeft(user, "@"), strconv.FormatInt(int64(to/time.Second), 10)}
|
||||
if reason != "" {
|
||||
cmd = append(cmd, reason)
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.WriteMessage(&irc.Message{
|
||||
Command: "PRIVMSG",
|
||||
Params: []string{
|
||||
if err = botTwitchClient.BanUser(
|
||||
plugins.DeriveChannel(m, eventData),
|
||||
strings.Join(cmd, " "),
|
||||
},
|
||||
}); err != nil {
|
||||
return false, errors.Wrap(err, "sending command")
|
||||
strings.TrimLeft(user, "@"),
|
||||
to,
|
||||
reason,
|
||||
); err != nil {
|
||||
return false, errors.Wrap(err, "executing user ban")
|
||||
}
|
||||
}
|
||||
|
||||
lvl.Cooldown = cooldown
|
||||
|
|
|
@ -18,6 +18,7 @@ const (
|
|||
var (
|
||||
db database.Connector
|
||||
formatMessage plugins.MsgFormatter
|
||||
send plugins.SendMessageFunc
|
||||
|
||||
ptrStringEmpty = func(v string) *string { return &v }("")
|
||||
ptrStringOutFormat = func(v string) *string { return &v }("Quote #{{ .index }}: {{ .quote }}")
|
||||
|
@ -31,6 +32,7 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
}
|
||||
|
||||
formatMessage = args.FormatMessage
|
||||
send = args.SendMessage
|
||||
|
||||
args.RegisterActor(actorName, func() plugins.Actor { return &actor{} })
|
||||
|
||||
|
@ -154,7 +156,7 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
|
|||
}
|
||||
|
||||
return false, errors.Wrap(
|
||||
c.WriteMessage(&irc.Message{
|
||||
send(&irc.Message{
|
||||
Command: "PRIVMSG",
|
||||
Params: []string{
|
||||
plugins.DeriveChannel(m, eventData),
|
||||
|
|
|
@ -9,10 +9,14 @@ import (
|
|||
|
||||
const actorName = "raw"
|
||||
|
||||
var formatMessage plugins.MsgFormatter
|
||||
var (
|
||||
formatMessage plugins.MsgFormatter
|
||||
send plugins.SendMessageFunc
|
||||
)
|
||||
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
formatMessage = args.FormatMessage
|
||||
send = args.SendMessage
|
||||
|
||||
args.RegisterActor(actorName, func() plugins.Actor { return &actor{} })
|
||||
|
||||
|
@ -51,7 +55,7 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
|
|||
}
|
||||
|
||||
return false, errors.Wrap(
|
||||
c.WriteMessage(msg),
|
||||
send(msg),
|
||||
"sending raw message",
|
||||
)
|
||||
}
|
||||
|
|
|
@ -15,12 +15,14 @@ const actorName = "respond"
|
|||
|
||||
var (
|
||||
formatMessage plugins.MsgFormatter
|
||||
send plugins.SendMessageFunc
|
||||
|
||||
ptrBoolFalse = func(v bool) *bool { return &v }(false)
|
||||
)
|
||||
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
formatMessage = args.FormatMessage
|
||||
send = args.SendMessage
|
||||
|
||||
args.RegisterActor(actorName, func() plugins.Actor { return &actor{} })
|
||||
|
||||
|
@ -111,7 +113,7 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
|
|||
}
|
||||
|
||||
return false, errors.Wrap(
|
||||
c.WriteMessage(ircMessage),
|
||||
send(ircMessage),
|
||||
"sending response",
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,24 +1,29 @@
|
|||
package timeout
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-irc/irc"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/Luzifer/twitch-bot/v2/pkg/twitch"
|
||||
"github.com/Luzifer/twitch-bot/v2/plugins"
|
||||
)
|
||||
|
||||
const actorName = "timeout"
|
||||
|
||||
var (
|
||||
botTwitchClient *twitch.Client
|
||||
formatMessage plugins.MsgFormatter
|
||||
ptrStringEmpty = func(v string) *string { return &v }("")
|
||||
|
||||
timeoutChatcommandRegex = regexp.MustCompile(`^/timeout +([^\s]+) +([0-9]+) +(.+)$`)
|
||||
)
|
||||
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
botTwitchClient = args.GetTwitchClient()
|
||||
formatMessage = args.FormatMessage
|
||||
|
||||
args.RegisterActor(actorName, func() plugins.Actor { return &actor{} })
|
||||
|
@ -43,43 +48,34 @@ func Register(args plugins.RegistrationArguments) error {
|
|||
Description: "Reason why the user was timed out",
|
||||
Key: "reason",
|
||||
Name: "Reason",
|
||||
Optional: true,
|
||||
Optional: false,
|
||||
SupportTemplate: true,
|
||||
Type: plugins.ActionDocumentationFieldTypeString,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
args.RegisterMessageModFunc("/timeout", handleChatCommand)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type actor struct{}
|
||||
|
||||
func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||
cmd := []string{
|
||||
"/timeout",
|
||||
plugins.DeriveUser(m, eventData),
|
||||
strconv.FormatInt(int64(attrs.MustDuration("duration", nil)/time.Second), 10),
|
||||
}
|
||||
|
||||
reason, err := formatMessage(attrs.MustString("reason", ptrStringEmpty), m, r, eventData)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "executing reason template")
|
||||
}
|
||||
|
||||
if reason != "" {
|
||||
cmd = append(cmd, reason)
|
||||
}
|
||||
|
||||
return false, errors.Wrap(
|
||||
c.WriteMessage(&irc.Message{
|
||||
Command: "PRIVMSG",
|
||||
Params: []string{
|
||||
botTwitchClient.BanUser(
|
||||
plugins.DeriveChannel(m, eventData),
|
||||
strings.Join(cmd, " "),
|
||||
},
|
||||
}),
|
||||
"sending timeout",
|
||||
plugins.DeriveUser(m, eventData),
|
||||
attrs.MustDuration("duration", nil),
|
||||
reason,
|
||||
),
|
||||
"executing timeout",
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -93,3 +89,23 @@ func (a actor) Validate(attrs *plugins.FieldCollection) (err error) {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleChatCommand(m *irc.Message) error {
|
||||
channel := plugins.DeriveChannel(m, nil)
|
||||
|
||||
matches := timeoutChatcommandRegex.FindStringSubmatch(m.Trailing())
|
||||
if matches == nil {
|
||||
return errors.New("timeout message does not match required format")
|
||||
}
|
||||
|
||||
duration, err := strconv.ParseInt(matches[2], 10, 64)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "parsing timeout duration")
|
||||
}
|
||||
|
||||
if err = botTwitchClient.BanUser(channel, matches[1], time.Duration(duration)*time.Second, matches[3]); err != nil {
|
||||
return errors.Wrap(err, "executing timeout")
|
||||
}
|
||||
|
||||
return plugins.ErrSkipSendingMessage
|
||||
}
|
||||
|
|
|
@ -1,19 +1,22 @@
|
|||
package whisper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-irc/irc"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/Luzifer/twitch-bot/v2/pkg/twitch"
|
||||
"github.com/Luzifer/twitch-bot/v2/plugins"
|
||||
)
|
||||
|
||||
const actorName = "whisper"
|
||||
|
||||
var formatMessage plugins.MsgFormatter
|
||||
var (
|
||||
botTwitchClient *twitch.Client
|
||||
formatMessage plugins.MsgFormatter
|
||||
)
|
||||
|
||||
func Register(args plugins.RegistrationArguments) error {
|
||||
botTwitchClient = args.GetTwitchClient()
|
||||
formatMessage = args.FormatMessage
|
||||
|
||||
args.RegisterActor(actorName, func() plugins.Actor { return &actor{} })
|
||||
|
@ -61,16 +64,8 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
|
|||
return false, errors.Wrap(err, "preparing whisper message")
|
||||
}
|
||||
|
||||
channel := "#tmijs" // As a fallback, copied from tmi.js
|
||||
|
||||
return false, errors.Wrap(
|
||||
c.WriteMessage(&irc.Message{
|
||||
Command: "PRIVMSG",
|
||||
Params: []string{
|
||||
channel,
|
||||
fmt.Sprintf("/w %s %s", to, msg),
|
||||
},
|
||||
}),
|
||||
botTwitchClient.SendWhisper(to, msg),
|
||||
"sending whisper",
|
||||
)
|
||||
}
|
||||
|
|
2
irc.go
2
irc.go
|
@ -55,7 +55,7 @@ type ircHandler struct {
|
|||
func newIRCHandler() (*ircHandler, error) {
|
||||
h := new(ircHandler)
|
||||
|
||||
username, err := twitchClient.GetAuthorizedUsername()
|
||||
_, username, err := twitchClient.GetAuthorizedUser()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "fetching username")
|
||||
}
|
||||
|
|
85
pkg/twitch/channels.go
Normal file
85
pkg/twitch/channels.go
Normal file
|
@ -0,0 +1,85 @@
|
|||
package twitch
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (c *Client) ModifyChannelInformation(ctx context.Context, broadcasterName string, game, title *string) error {
|
||||
if game == nil && title == nil {
|
||||
return errors.New("netiher game nor title provided")
|
||||
}
|
||||
|
||||
broadcaster, err := c.GetIDForUsername(broadcasterName)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "getting ID for broadcaster name")
|
||||
}
|
||||
|
||||
data := struct {
|
||||
GameID *string `json:"game_id,omitempty"`
|
||||
Title *string `json:"title,omitempty"`
|
||||
}{
|
||||
Title: title,
|
||||
}
|
||||
|
||||
switch {
|
||||
case game == nil:
|
||||
// We don't set the GameID
|
||||
|
||||
case (*game)[0] == '@':
|
||||
// We got an ID and don't need to resolve
|
||||
gameID := (*game)[1:]
|
||||
data.GameID = &gameID
|
||||
|
||||
default:
|
||||
categories, err := c.SearchCategories(ctx, *game)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "searching for game")
|
||||
}
|
||||
|
||||
switch len(categories) {
|
||||
case 0:
|
||||
return errors.New("no matching game found")
|
||||
|
||||
case 1:
|
||||
data.GameID = &categories[0].ID
|
||||
|
||||
default:
|
||||
// Multiple matches: Search for exact one
|
||||
for _, c := range categories {
|
||||
if c.Name == *game {
|
||||
gid := c.ID
|
||||
data.GameID = &gid
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if data.GameID == nil {
|
||||
// No exact match found: This is an error
|
||||
return errors.New("no exact game match found")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body := new(bytes.Buffer)
|
||||
if err := json.NewEncoder(body).Encode(data); err != nil {
|
||||
return errors.Wrap(err, "encoding payload")
|
||||
}
|
||||
|
||||
return errors.Wrap(
|
||||
c.request(clientRequestOpts{
|
||||
AuthType: authTypeBearerToken,
|
||||
Body: body,
|
||||
Context: ctx,
|
||||
Method: http.MethodPatch,
|
||||
OKStatus: http.StatusNoContent,
|
||||
URL: fmt.Sprintf("https://api.twitch.tv/helix/channels?broadcaster_id=%s", broadcaster),
|
||||
}),
|
||||
"executing request",
|
||||
)
|
||||
}
|
55
pkg/twitch/chat.go
Normal file
55
pkg/twitch/chat.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
package twitch
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// SendChatAnnouncement sends an announcement in the specified
|
||||
// channel with the given message. Colors must be blue, green,
|
||||
// orange, purple or primary (empty color = primary)
|
||||
func (c *Client) SendChatAnnouncement(channel, color, message string) error {
|
||||
var payload struct {
|
||||
Color string `json:"color,omitempty"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
payload.Color = color
|
||||
payload.Message = message
|
||||
|
||||
botID, _, err := c.GetAuthorizedUser()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "getting bot user-id")
|
||||
}
|
||||
|
||||
channelID, err := c.GetIDForUsername(strings.TrimLeft(channel, "#@"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "getting channel user-id")
|
||||
}
|
||||
|
||||
body := new(bytes.Buffer)
|
||||
if err = json.NewEncoder(body).Encode(payload); err != nil {
|
||||
return errors.Wrap(err, "encoding payload")
|
||||
}
|
||||
|
||||
return errors.Wrap(
|
||||
c.request(clientRequestOpts{
|
||||
AuthType: authTypeBearerToken,
|
||||
Context: context.Background(),
|
||||
Method: http.MethodPost,
|
||||
OKStatus: http.StatusNoContent,
|
||||
Body: body,
|
||||
URL: fmt.Sprintf(
|
||||
"https://api.twitch.tv/helix/chat/announcements?broadcaster_id=%s&moderator_id=%s",
|
||||
channelID, botID,
|
||||
),
|
||||
}),
|
||||
"executing request",
|
||||
)
|
||||
}
|
|
@ -9,6 +9,7 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
@ -388,10 +389,89 @@ func (e *EventSubClient) RegisterEventSubHooks(event string, condition EventSubC
|
|||
return func() { e.unregisterCallback(cacheKey, cbKey) }, nil
|
||||
}
|
||||
|
||||
func (c *Client) createEventSubSubscription(ctx context.Context, sub eventSubSubscription) (*eventSubSubscription, error) {
|
||||
var (
|
||||
buf = new(bytes.Buffer)
|
||||
resp struct {
|
||||
Total int64 `json:"total"`
|
||||
Data []eventSubSubscription `json:"data"`
|
||||
Pagination struct {
|
||||
Cursor string `json:"cursor"`
|
||||
} `json:"pagination"`
|
||||
}
|
||||
)
|
||||
|
||||
if err := json.NewEncoder(buf).Encode(sub); err != nil {
|
||||
return nil, errors.Wrap(err, "assemble subscribe payload")
|
||||
}
|
||||
|
||||
if err := c.request(clientRequestOpts{
|
||||
AuthType: authTypeAppAccessToken,
|
||||
Body: buf,
|
||||
Context: ctx,
|
||||
Method: http.MethodPost,
|
||||
OKStatus: http.StatusAccepted,
|
||||
Out: &resp,
|
||||
URL: "https://api.twitch.tv/helix/eventsub/subscriptions",
|
||||
}); err != nil {
|
||||
return nil, errors.Wrap(err, "executing request")
|
||||
}
|
||||
|
||||
return &resp.Data[0], nil
|
||||
}
|
||||
|
||||
func (c *Client) deleteEventSubSubscription(ctx context.Context, id string) error {
|
||||
return errors.Wrap(c.request(clientRequestOpts{
|
||||
AuthType: authTypeAppAccessToken,
|
||||
Context: ctx,
|
||||
Method: http.MethodDelete,
|
||||
OKStatus: http.StatusNoContent,
|
||||
URL: fmt.Sprintf("https://api.twitch.tv/helix/eventsub/subscriptions?id=%s", id),
|
||||
}), "executing request")
|
||||
}
|
||||
|
||||
func (e *EventSubClient) fullAPIurl() string {
|
||||
return strings.Join([]string{e.apiURL, e.secretHandle}, "/")
|
||||
}
|
||||
|
||||
func (c *Client) getEventSubSubscriptions(ctx context.Context) ([]eventSubSubscription, error) {
|
||||
var (
|
||||
out []eventSubSubscription
|
||||
params = make(url.Values)
|
||||
resp struct {
|
||||
Total int64 `json:"total"`
|
||||
Data []eventSubSubscription `json:"data"`
|
||||
Pagination struct {
|
||||
Cursor string `json:"cursor"`
|
||||
} `json:"pagination"`
|
||||
}
|
||||
)
|
||||
|
||||
for {
|
||||
if err := c.request(clientRequestOpts{
|
||||
AuthType: authTypeAppAccessToken,
|
||||
Context: ctx,
|
||||
Method: http.MethodGet,
|
||||
OKStatus: http.StatusOK,
|
||||
Out: &resp,
|
||||
URL: fmt.Sprintf("https://api.twitch.tv/helix/eventsub/subscriptions?%s", params.Encode()),
|
||||
}); err != nil {
|
||||
return nil, errors.Wrap(err, "executing request")
|
||||
}
|
||||
|
||||
out = append(out, resp.Data...)
|
||||
|
||||
if resp.Pagination.Cursor == "" {
|
||||
break
|
||||
}
|
||||
|
||||
params.Set("after", resp.Pagination.Cursor)
|
||||
resp.Pagination.Cursor = "" // Clear from struct as struct is reused
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (e *EventSubClient) unregisterCallback(cacheKey, cbKey string) {
|
||||
e.subscriptionsLock.RLock()
|
||||
regSub, ok := e.subscriptions[cacheKey]
|
||||
|
|
140
pkg/twitch/moderation.go
Normal file
140
pkg/twitch/moderation.go
Normal file
|
@ -0,0 +1,140 @@
|
|||
package twitch
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const maxTimeoutDuration = 1209600 * time.Second
|
||||
|
||||
// BanUser bans or timeouts a user in the given channel. Setting the
|
||||
// duration to 0 will result in a ban, setting if greater than 0 will
|
||||
// result in a timeout. The timeout is automatically converted to
|
||||
// full seconds. The timeout duration must be less than 1209600s.
|
||||
func (c *Client) BanUser(channel, username string, duration time.Duration, reason string) error {
|
||||
var payload struct {
|
||||
Data struct {
|
||||
Duration int64 `json:"duration,omitempty"`
|
||||
Reason string `json:"reason"`
|
||||
UserID string `json:"user_id"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
if duration > maxTimeoutDuration {
|
||||
return errors.New("timeout duration exceeds maximum")
|
||||
}
|
||||
|
||||
payload.Data.Duration = int64(duration / time.Second)
|
||||
payload.Data.Reason = reason
|
||||
|
||||
botID, _, err := c.GetAuthorizedUser()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "getting bot user-id")
|
||||
}
|
||||
|
||||
channelID, err := c.GetIDForUsername(strings.TrimLeft(channel, "#@"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "getting channel user-id")
|
||||
}
|
||||
|
||||
if payload.Data.UserID, err = c.GetIDForUsername(username); err != nil {
|
||||
return errors.Wrap(err, "getting target user-id")
|
||||
}
|
||||
|
||||
body := new(bytes.Buffer)
|
||||
if err = json.NewEncoder(body).Encode(payload); err != nil {
|
||||
return errors.Wrap(err, "encoding payload")
|
||||
}
|
||||
|
||||
return errors.Wrap(
|
||||
c.request(clientRequestOpts{
|
||||
AuthType: authTypeBearerToken,
|
||||
Context: context.Background(),
|
||||
Method: http.MethodPost,
|
||||
OKStatus: http.StatusOK,
|
||||
Body: body,
|
||||
URL: fmt.Sprintf(
|
||||
"https://api.twitch.tv/helix/moderation/bans?broadcaster_id=%s&moderator_id=%s",
|
||||
channelID, botID,
|
||||
),
|
||||
}),
|
||||
"executing ban request",
|
||||
)
|
||||
}
|
||||
|
||||
// DeleteMessage deletes one or all messages from the specified chat.
|
||||
// If no messageID is given all messages are deleted. If a message ID
|
||||
// is given the message must be no older than 6 hours and it must not
|
||||
// be posted by broadcaster or moderator.
|
||||
func (c *Client) DeleteMessage(channel, messageID string) error {
|
||||
botID, _, err := c.GetAuthorizedUser()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "getting bot user-id")
|
||||
}
|
||||
|
||||
channelID, err := c.GetIDForUsername(strings.TrimLeft(channel, "#@"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "getting channel user-id")
|
||||
}
|
||||
|
||||
params := make(url.Values)
|
||||
params.Set("broadcaster_id", channelID)
|
||||
params.Set("moderator_id", botID)
|
||||
if messageID != "" {
|
||||
params.Set("message_id", messageID)
|
||||
}
|
||||
|
||||
return errors.Wrap(
|
||||
c.request(clientRequestOpts{
|
||||
AuthType: authTypeBearerToken,
|
||||
Context: context.Background(),
|
||||
Method: http.MethodDelete,
|
||||
OKStatus: http.StatusNoContent,
|
||||
URL: fmt.Sprintf(
|
||||
"https://api.twitch.tv/helix/moderation/chat?%s",
|
||||
params.Encode(),
|
||||
),
|
||||
}),
|
||||
"executing delete request",
|
||||
)
|
||||
}
|
||||
|
||||
// UnbanUser removes a timeout or ban given to the user in the channel
|
||||
func (c *Client) UnbanUser(channel, username string) error {
|
||||
botID, _, err := c.GetAuthorizedUser()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "getting bot user-id")
|
||||
}
|
||||
|
||||
channelID, err := c.GetIDForUsername(strings.TrimLeft(channel, "#@"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "getting channel user-id")
|
||||
}
|
||||
|
||||
userID, err := c.GetIDForUsername(username)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "getting target user-id")
|
||||
}
|
||||
|
||||
return errors.Wrap(
|
||||
c.request(clientRequestOpts{
|
||||
AuthType: authTypeBearerToken,
|
||||
Context: context.Background(),
|
||||
Method: http.MethodDelete,
|
||||
OKStatus: http.StatusNoContent,
|
||||
URL: fmt.Sprintf(
|
||||
"https://api.twitch.tv/helix/moderation/bans?broadcaster_id=%s&moderator_id=%s&user_id=%s",
|
||||
channelID, botID, userID,
|
||||
),
|
||||
}),
|
||||
"executing unban request",
|
||||
)
|
||||
}
|
|
@ -2,20 +2,27 @@ package twitch
|
|||
|
||||
const (
|
||||
// API Scopes
|
||||
ScopeChannelManageRedemptions = "channel:manage:redemptions"
|
||||
ScopeChannelReadRedemptions = "channel:read:redemptions"
|
||||
ScopeChannelEditCommercial = "channel:edit:commercial"
|
||||
ScopeChannelManageBroadcast = "channel:manage:broadcast"
|
||||
ScopeChannelManageModerators = "channel:manage:moderators"
|
||||
ScopeChannelManagePolls = "channel:manage:polls"
|
||||
ScopeChannelManagePredictions = "channel:manage:predictions"
|
||||
ScopeChannelManageRaids = "channel:manage:raids"
|
||||
ScopeChannelManageRedemptions = "channel:manage:redemptions"
|
||||
ScopeChannelManageVIPS = "channel:manage:vips"
|
||||
ScopeChannelManageWhispers = "user:manage:whispers"
|
||||
ScopeChannelReadRedemptions = "channel:read:redemptions"
|
||||
ScopeModeratorManageAnnoucements = "moderator:manage:announcements"
|
||||
ScopeModeratorManageBannedUsers = "moderator:manage:banned_users"
|
||||
ScopeModeratorManageChatMessages = "moderator:manage:chat_messages"
|
||||
ScopeModeratorManageChatSettings = "moderator:manage:chat_settings"
|
||||
ScopeUserManageChatColor = "user:manage:chat_color"
|
||||
|
||||
// Deprecated v5 scope but used in chat
|
||||
ScopeV5ChannelEditor = "channel_editor"
|
||||
|
||||
// Chat Scopes
|
||||
ScopeChannelModerate = "channel:moderate" // Perform moderation actions in a channel. The user requesting the scope must be a moderator in the channel.
|
||||
ScopeChatEdit = "chat:edit" // Send live stream chat and rooms messages.
|
||||
ScopeChatRead = "chat:read" // View live stream chat and rooms messages.
|
||||
ScopeWhisperRead = "whispers:read" // View your whisper messages.
|
||||
ScopeWhisperEdit = "whispers:edit" // Send whisper messages.
|
||||
)
|
||||
|
|
57
pkg/twitch/search.go
Normal file
57
pkg/twitch/search.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
package twitch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type (
|
||||
Category struct {
|
||||
BoxArtURL string `json:"box_art_url"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
)
|
||||
|
||||
func (c *Client) SearchCategories(ctx context.Context, name string) ([]Category, error) {
|
||||
var out []Category
|
||||
|
||||
params := make(url.Values)
|
||||
params.Set("query", name)
|
||||
params.Set("first", "100")
|
||||
|
||||
var resp struct {
|
||||
Data []Category `json:"data"`
|
||||
Pagination struct {
|
||||
Cursor string `json:"cursor"`
|
||||
} `json:"pagination"`
|
||||
}
|
||||
|
||||
for {
|
||||
if err := c.request(clientRequestOpts{
|
||||
AuthType: authTypeBearerToken,
|
||||
Context: ctx,
|
||||
Method: http.MethodGet,
|
||||
OKStatus: http.StatusOK,
|
||||
Out: &resp,
|
||||
URL: fmt.Sprintf("https://api.twitch.tv/helix/search/categories?%s", params.Encode()),
|
||||
}); err != nil {
|
||||
return nil, errors.Wrap(err, "executing request")
|
||||
}
|
||||
|
||||
out = append(out, resp.Data...)
|
||||
|
||||
if resp.Pagination.Cursor == "" {
|
||||
break
|
||||
}
|
||||
|
||||
params.Set("after", resp.Pagination.Cursor)
|
||||
resp.Pagination.Cursor = "" // Clear from struct as struct is reused
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
137
pkg/twitch/streams.go
Normal file
137
pkg/twitch/streams.go
Normal file
|
@ -0,0 +1,137 @@
|
|||
package twitch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type (
|
||||
StreamInfo struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
UserLogin string `json:"user_login"`
|
||||
UserName string `json:"user_name"`
|
||||
GameID string `json:"game_id"`
|
||||
GameName string `json:"game_name"`
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
ViewerCount int64 `json:"viewer_count"`
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
Language string `json:"language"`
|
||||
ThumbnailURL string `json:"thumbnail_url"`
|
||||
TagIds []string `json:"tag_ids"`
|
||||
IsMature bool `json:"is_mature"`
|
||||
}
|
||||
)
|
||||
|
||||
func (c *Client) GetCurrentStreamInfo(username string) (*StreamInfo, error) {
|
||||
cacheKey := []string{"currentStreamInfo", username}
|
||||
if si := c.apiCache.Get(cacheKey); si != nil {
|
||||
return si.(*StreamInfo), nil
|
||||
}
|
||||
|
||||
id, err := c.GetIDForUsername(username)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getting ID for username")
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
Data []*StreamInfo `json:"data"`
|
||||
}
|
||||
|
||||
if err := c.request(clientRequestOpts{
|
||||
AuthType: authTypeBearerToken,
|
||||
Context: context.Background(),
|
||||
Method: http.MethodGet,
|
||||
OKStatus: http.StatusOK,
|
||||
Out: &payload,
|
||||
URL: fmt.Sprintf("https://api.twitch.tv/helix/streams?user_id=%s", id),
|
||||
}); err != nil {
|
||||
return nil, errors.Wrap(err, "request channel info")
|
||||
}
|
||||
|
||||
if l := len(payload.Data); l != 1 {
|
||||
return nil, errors.Errorf("unexpected number of users returned: %d", l)
|
||||
}
|
||||
|
||||
// Stream-info can be changed at any moment, cache for a short period of time
|
||||
c.apiCache.Set(cacheKey, twitchMinCacheTime, payload.Data[0])
|
||||
|
||||
return payload.Data[0], nil
|
||||
}
|
||||
|
||||
func (c *Client) GetRecentStreamInfo(username string) (string, string, error) {
|
||||
cacheKey := []string{"recentStreamInfo", username}
|
||||
if d := c.apiCache.Get(cacheKey); d != nil {
|
||||
return d.([2]string)[0], d.([2]string)[1], nil
|
||||
}
|
||||
|
||||
id, err := c.GetIDForUsername(username)
|
||||
if err != nil {
|
||||
return "", "", errors.Wrap(err, "getting ID for username")
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
Data []struct {
|
||||
BroadcasterID string `json:"broadcaster_id"`
|
||||
GameID string `json:"game_id"`
|
||||
GameName string `json:"game_name"`
|
||||
Title string `json:"title"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
if err := c.request(clientRequestOpts{
|
||||
AuthType: authTypeBearerToken,
|
||||
Context: context.Background(),
|
||||
Method: http.MethodGet,
|
||||
OKStatus: http.StatusOK,
|
||||
Out: &payload,
|
||||
URL: fmt.Sprintf("https://api.twitch.tv/helix/channels?broadcaster_id=%s", id),
|
||||
}); err != nil {
|
||||
return "", "", errors.Wrap(err, "request channel info")
|
||||
}
|
||||
|
||||
if l := len(payload.Data); l != 1 {
|
||||
return "", "", errors.Errorf("unexpected number of users returned: %d", l)
|
||||
}
|
||||
|
||||
// Stream-info can be changed at any moment, cache for a short period of time
|
||||
c.apiCache.Set(cacheKey, twitchMinCacheTime, [2]string{payload.Data[0].GameName, payload.Data[0].Title})
|
||||
|
||||
return payload.Data[0].GameName, payload.Data[0].Title, nil
|
||||
}
|
||||
|
||||
func (c *Client) HasLiveStream(username string) (bool, error) {
|
||||
cacheKey := []string{"hasLiveStream", username}
|
||||
if d := c.apiCache.Get(cacheKey); d != nil {
|
||||
return d.(bool), nil
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
Data []struct {
|
||||
ID string `json:"id"`
|
||||
UserLogin string `json:"user_login"`
|
||||
Type string `json:"type"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
if err := c.request(clientRequestOpts{
|
||||
AuthType: authTypeBearerToken,
|
||||
Context: context.Background(),
|
||||
Method: http.MethodGet,
|
||||
OKStatus: http.StatusOK,
|
||||
Out: &payload,
|
||||
URL: fmt.Sprintf("https://api.twitch.tv/helix/streams?user_login=%s", username),
|
||||
}); err != nil {
|
||||
return false, errors.Wrap(err, "request stream info")
|
||||
}
|
||||
|
||||
// Live status might change recently, cache for one minute
|
||||
c.apiCache.Set(cacheKey, twitchMinCacheTime, len(payload.Data) == 1 && payload.Data[0].Type == "live")
|
||||
|
||||
return len(payload.Data) == 1 && payload.Data[0].Type == "live", nil
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
package twitch
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
|
@ -9,7 +8,6 @@ import (
|
|||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -37,12 +35,6 @@ const (
|
|||
)
|
||||
|
||||
type (
|
||||
Category struct {
|
||||
BoxArtURL string `json:"box_art_url"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
Client struct {
|
||||
clientID string
|
||||
clientSecret string
|
||||
|
@ -74,30 +66,6 @@ type (
|
|||
ExpiresIn int `json:"expires_in"`
|
||||
}
|
||||
|
||||
StreamInfo struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
UserLogin string `json:"user_login"`
|
||||
UserName string `json:"user_name"`
|
||||
GameID string `json:"game_id"`
|
||||
GameName string `json:"game_name"`
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
ViewerCount int64 `json:"viewer_count"`
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
Language string `json:"language"`
|
||||
ThumbnailURL string `json:"thumbnail_url"`
|
||||
TagIds []string `json:"tag_ids"`
|
||||
IsMature bool `json:"is_mature"`
|
||||
}
|
||||
|
||||
User struct {
|
||||
DisplayName string `json:"display_name"`
|
||||
ID string `json:"id"`
|
||||
Login string `json:"login"`
|
||||
ProfileImageURL string `json:"profile_image_url"`
|
||||
}
|
||||
|
||||
authType uint8
|
||||
|
||||
clientRequestOpts struct {
|
||||
|
@ -127,101 +95,6 @@ func New(clientID, clientSecret, accessToken, refreshToken string) *Client {
|
|||
|
||||
func (c *Client) APICache() *APICache { return c.apiCache }
|
||||
|
||||
func (c *Client) GetAuthorizedUsername() (string, error) {
|
||||
var payload struct {
|
||||
Data []User `json:"data"`
|
||||
}
|
||||
|
||||
if err := c.request(clientRequestOpts{
|
||||
AuthType: authTypeBearerToken,
|
||||
Context: context.Background(),
|
||||
Method: http.MethodGet,
|
||||
OKStatus: http.StatusOK,
|
||||
Out: &payload,
|
||||
URL: "https://api.twitch.tv/helix/users",
|
||||
}); err != nil {
|
||||
return "", errors.Wrap(err, "request channel info")
|
||||
}
|
||||
|
||||
if l := len(payload.Data); l != 1 {
|
||||
return "", errors.Errorf("unexpected number of users returned: %d", l)
|
||||
}
|
||||
|
||||
return payload.Data[0].Login, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetDisplayNameForUser(username string) (string, error) {
|
||||
cacheKey := []string{"displayNameForUsername", username}
|
||||
if d := c.apiCache.Get(cacheKey); d != nil {
|
||||
return d.(string), nil
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
Data []User `json:"data"`
|
||||
}
|
||||
|
||||
if err := c.request(clientRequestOpts{
|
||||
AuthType: authTypeAppAccessToken,
|
||||
Context: context.Background(),
|
||||
Method: http.MethodGet,
|
||||
Out: &payload,
|
||||
URL: fmt.Sprintf("https://api.twitch.tv/helix/users?login=%s", username),
|
||||
}); err != nil {
|
||||
return "", errors.Wrap(err, "request channel info")
|
||||
}
|
||||
|
||||
if l := len(payload.Data); l != 1 {
|
||||
return "", errors.Errorf("unexpected number of users returned: %d", l)
|
||||
}
|
||||
|
||||
// The DisplayName for an username will not change (often), cache for a decent time
|
||||
c.apiCache.Set(cacheKey, time.Hour, payload.Data[0].DisplayName)
|
||||
|
||||
return payload.Data[0].DisplayName, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetFollowDate(from, to string) (time.Time, error) {
|
||||
cacheKey := []string{"followDate", from, to}
|
||||
if d := c.apiCache.Get(cacheKey); d != nil {
|
||||
return d.(time.Time), nil
|
||||
}
|
||||
|
||||
fromID, err := c.GetIDForUsername(from)
|
||||
if err != nil {
|
||||
return time.Time{}, errors.Wrap(err, "getting id for 'from' user")
|
||||
}
|
||||
toID, err := c.GetIDForUsername(to)
|
||||
if err != nil {
|
||||
return time.Time{}, errors.Wrap(err, "getting id for 'to' user")
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
Data []struct {
|
||||
FollowedAt time.Time `json:"followed_at"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
if err := c.request(clientRequestOpts{
|
||||
AuthType: authTypeAppAccessToken,
|
||||
Context: context.Background(),
|
||||
Method: http.MethodGet,
|
||||
OKStatus: http.StatusOK,
|
||||
Out: &payload,
|
||||
URL: fmt.Sprintf("https://api.twitch.tv/helix/users/follows?to_id=%s&from_id=%s", toID, fromID),
|
||||
}); err != nil {
|
||||
return time.Time{}, errors.Wrap(err, "request follow info")
|
||||
}
|
||||
|
||||
if l := len(payload.Data); l != 1 {
|
||||
return time.Time{}, errors.Errorf("unexpected number of records returned: %d", l)
|
||||
}
|
||||
|
||||
// Follow date will not change that often, cache for a long time
|
||||
c.apiCache.Set(cacheKey, timeDay, payload.Data[0].FollowedAt)
|
||||
|
||||
return payload.Data[0].FollowedAt, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetToken() (string, error) {
|
||||
if err := c.ValidateToken(context.Background(), false); err != nil {
|
||||
if err = c.RefreshToken(); err != nil {
|
||||
|
@ -234,299 +107,6 @@ func (c *Client) GetToken() (string, error) {
|
|||
return c.accessToken, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetUserInformation(user string) (*User, error) {
|
||||
var (
|
||||
out User
|
||||
param = "login"
|
||||
payload struct {
|
||||
Data []User `json:"data"`
|
||||
}
|
||||
)
|
||||
|
||||
cacheKey := []string{"userInformation", user}
|
||||
if d := c.apiCache.Get(cacheKey); d != nil {
|
||||
out = d.(User)
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
if _, err := strconv.ParseInt(user, 10, 64); err == nil {
|
||||
param = "id"
|
||||
}
|
||||
|
||||
if err := c.request(clientRequestOpts{
|
||||
AuthType: authTypeAppAccessToken,
|
||||
Context: context.Background(),
|
||||
Method: http.MethodGet,
|
||||
OKStatus: http.StatusOK,
|
||||
Out: &payload,
|
||||
URL: fmt.Sprintf("https://api.twitch.tv/helix/users?%s=%s", param, user),
|
||||
}); err != nil {
|
||||
return nil, errors.Wrap(err, "request user info")
|
||||
}
|
||||
|
||||
if l := len(payload.Data); l != 1 {
|
||||
return nil, errors.Errorf("unexpected number of records returned: %d", l)
|
||||
}
|
||||
|
||||
// Follow date will not change that often, cache for a long time
|
||||
c.apiCache.Set(cacheKey, timeDay, payload.Data[0])
|
||||
out = payload.Data[0]
|
||||
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (c *Client) SearchCategories(ctx context.Context, name string) ([]Category, error) {
|
||||
var out []Category
|
||||
|
||||
params := make(url.Values)
|
||||
params.Set("query", name)
|
||||
params.Set("first", "100")
|
||||
|
||||
var resp struct {
|
||||
Data []Category `json:"data"`
|
||||
Pagination struct {
|
||||
Cursor string `json:"cursor"`
|
||||
} `json:"pagination"`
|
||||
}
|
||||
|
||||
for {
|
||||
if err := c.request(clientRequestOpts{
|
||||
AuthType: authTypeBearerToken,
|
||||
Context: ctx,
|
||||
Method: http.MethodGet,
|
||||
OKStatus: http.StatusOK,
|
||||
Out: &resp,
|
||||
URL: fmt.Sprintf("https://api.twitch.tv/helix/search/categories?%s", params.Encode()),
|
||||
}); err != nil {
|
||||
return nil, errors.Wrap(err, "executing request")
|
||||
}
|
||||
|
||||
out = append(out, resp.Data...)
|
||||
|
||||
if resp.Pagination.Cursor == "" {
|
||||
break
|
||||
}
|
||||
|
||||
params.Set("after", resp.Pagination.Cursor)
|
||||
resp.Pagination.Cursor = "" // Clear from struct as struct is reused
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *Client) HasLiveStream(username string) (bool, error) {
|
||||
cacheKey := []string{"hasLiveStream", username}
|
||||
if d := c.apiCache.Get(cacheKey); d != nil {
|
||||
return d.(bool), nil
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
Data []struct {
|
||||
ID string `json:"id"`
|
||||
UserLogin string `json:"user_login"`
|
||||
Type string `json:"type"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
if err := c.request(clientRequestOpts{
|
||||
AuthType: authTypeBearerToken,
|
||||
Context: context.Background(),
|
||||
Method: http.MethodGet,
|
||||
OKStatus: http.StatusOK,
|
||||
Out: &payload,
|
||||
URL: fmt.Sprintf("https://api.twitch.tv/helix/streams?user_login=%s", username),
|
||||
}); err != nil {
|
||||
return false, errors.Wrap(err, "request stream info")
|
||||
}
|
||||
|
||||
// Live status might change recently, cache for one minute
|
||||
c.apiCache.Set(cacheKey, twitchMinCacheTime, len(payload.Data) == 1 && payload.Data[0].Type == "live")
|
||||
|
||||
return len(payload.Data) == 1 && payload.Data[0].Type == "live", nil
|
||||
}
|
||||
|
||||
func (c *Client) GetCurrentStreamInfo(username string) (*StreamInfo, error) {
|
||||
cacheKey := []string{"currentStreamInfo", username}
|
||||
if si := c.apiCache.Get(cacheKey); si != nil {
|
||||
return si.(*StreamInfo), nil
|
||||
}
|
||||
|
||||
id, err := c.GetIDForUsername(username)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getting ID for username")
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
Data []*StreamInfo `json:"data"`
|
||||
}
|
||||
|
||||
if err := c.request(clientRequestOpts{
|
||||
AuthType: authTypeBearerToken,
|
||||
Context: context.Background(),
|
||||
Method: http.MethodGet,
|
||||
OKStatus: http.StatusOK,
|
||||
Out: &payload,
|
||||
URL: fmt.Sprintf("https://api.twitch.tv/helix/streams?user_id=%s", id),
|
||||
}); err != nil {
|
||||
return nil, errors.Wrap(err, "request channel info")
|
||||
}
|
||||
|
||||
if l := len(payload.Data); l != 1 {
|
||||
return nil, errors.Errorf("unexpected number of users returned: %d", l)
|
||||
}
|
||||
|
||||
// Stream-info can be changed at any moment, cache for a short period of time
|
||||
c.apiCache.Set(cacheKey, twitchMinCacheTime, payload.Data[0])
|
||||
|
||||
return payload.Data[0], nil
|
||||
}
|
||||
|
||||
func (c *Client) GetIDForUsername(username string) (string, error) {
|
||||
cacheKey := []string{"idForUsername", username}
|
||||
if d := c.apiCache.Get(cacheKey); d != nil {
|
||||
return d.(string), nil
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
Data []User `json:"data"`
|
||||
}
|
||||
|
||||
if err := c.request(clientRequestOpts{
|
||||
AuthType: authTypeAppAccessToken,
|
||||
Context: context.Background(),
|
||||
Method: http.MethodGet,
|
||||
OKStatus: http.StatusOK,
|
||||
Out: &payload,
|
||||
URL: fmt.Sprintf("https://api.twitch.tv/helix/users?login=%s", username),
|
||||
}); err != nil {
|
||||
return "", errors.Wrap(err, "request channel info")
|
||||
}
|
||||
|
||||
if l := len(payload.Data); l != 1 {
|
||||
return "", errors.Errorf("unexpected number of users returned: %d", l)
|
||||
}
|
||||
|
||||
// The ID for an username will not change (often), cache for a long time
|
||||
c.apiCache.Set(cacheKey, timeDay, payload.Data[0].ID)
|
||||
|
||||
return payload.Data[0].ID, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetRecentStreamInfo(username string) (string, string, error) {
|
||||
cacheKey := []string{"recentStreamInfo", username}
|
||||
if d := c.apiCache.Get(cacheKey); d != nil {
|
||||
return d.([2]string)[0], d.([2]string)[1], nil
|
||||
}
|
||||
|
||||
id, err := c.GetIDForUsername(username)
|
||||
if err != nil {
|
||||
return "", "", errors.Wrap(err, "getting ID for username")
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
Data []struct {
|
||||
BroadcasterID string `json:"broadcaster_id"`
|
||||
GameID string `json:"game_id"`
|
||||
GameName string `json:"game_name"`
|
||||
Title string `json:"title"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
if err := c.request(clientRequestOpts{
|
||||
AuthType: authTypeBearerToken,
|
||||
Context: context.Background(),
|
||||
Method: http.MethodGet,
|
||||
OKStatus: http.StatusOK,
|
||||
Out: &payload,
|
||||
URL: fmt.Sprintf("https://api.twitch.tv/helix/channels?broadcaster_id=%s", id),
|
||||
}); err != nil {
|
||||
return "", "", errors.Wrap(err, "request channel info")
|
||||
}
|
||||
|
||||
if l := len(payload.Data); l != 1 {
|
||||
return "", "", errors.Errorf("unexpected number of users returned: %d", l)
|
||||
}
|
||||
|
||||
// Stream-info can be changed at any moment, cache for a short period of time
|
||||
c.apiCache.Set(cacheKey, twitchMinCacheTime, [2]string{payload.Data[0].GameName, payload.Data[0].Title})
|
||||
|
||||
return payload.Data[0].GameName, payload.Data[0].Title, nil
|
||||
}
|
||||
|
||||
func (c *Client) ModifyChannelInformation(ctx context.Context, broadcasterName string, game, title *string) error {
|
||||
if game == nil && title == nil {
|
||||
return errors.New("netiher game nor title provided")
|
||||
}
|
||||
|
||||
broadcaster, err := c.GetIDForUsername(broadcasterName)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "getting ID for broadcaster name")
|
||||
}
|
||||
|
||||
data := struct {
|
||||
GameID *string `json:"game_id,omitempty"`
|
||||
Title *string `json:"title,omitempty"`
|
||||
}{
|
||||
Title: title,
|
||||
}
|
||||
|
||||
switch {
|
||||
case game == nil:
|
||||
// We don't set the GameID
|
||||
|
||||
case (*game)[0] == '@':
|
||||
// We got an ID and don't need to resolve
|
||||
gameID := (*game)[1:]
|
||||
data.GameID = &gameID
|
||||
|
||||
default:
|
||||
categories, err := c.SearchCategories(ctx, *game)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "searching for game")
|
||||
}
|
||||
|
||||
switch len(categories) {
|
||||
case 0:
|
||||
return errors.New("no matching game found")
|
||||
|
||||
case 1:
|
||||
data.GameID = &categories[0].ID
|
||||
|
||||
default:
|
||||
// Multiple matches: Search for exact one
|
||||
for _, c := range categories {
|
||||
if c.Name == *game {
|
||||
gid := c.ID
|
||||
data.GameID = &gid
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if data.GameID == nil {
|
||||
// No exact match found: This is an error
|
||||
return errors.New("no exact game match found")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body := new(bytes.Buffer)
|
||||
if err := json.NewEncoder(body).Encode(data); err != nil {
|
||||
return errors.Wrap(err, "encoding payload")
|
||||
}
|
||||
|
||||
return errors.Wrap(
|
||||
c.request(clientRequestOpts{
|
||||
AuthType: authTypeBearerToken,
|
||||
Body: body,
|
||||
Context: ctx,
|
||||
Method: http.MethodPatch,
|
||||
OKStatus: http.StatusNoContent,
|
||||
URL: fmt.Sprintf("https://api.twitch.tv/helix/channels?broadcaster_id=%s", broadcaster),
|
||||
}),
|
||||
"executing request",
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Client) RefreshToken() error {
|
||||
if c.refreshToken == "" {
|
||||
return errors.New("no refresh token set")
|
||||
|
@ -630,85 +210,6 @@ func (c *Client) ValidateToken(ctx context.Context, force bool) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) createEventSubSubscription(ctx context.Context, sub eventSubSubscription) (*eventSubSubscription, error) {
|
||||
var (
|
||||
buf = new(bytes.Buffer)
|
||||
resp struct {
|
||||
Total int64 `json:"total"`
|
||||
Data []eventSubSubscription `json:"data"`
|
||||
Pagination struct {
|
||||
Cursor string `json:"cursor"`
|
||||
} `json:"pagination"`
|
||||
}
|
||||
)
|
||||
|
||||
if err := json.NewEncoder(buf).Encode(sub); err != nil {
|
||||
return nil, errors.Wrap(err, "assemble subscribe payload")
|
||||
}
|
||||
|
||||
if err := c.request(clientRequestOpts{
|
||||
AuthType: authTypeAppAccessToken,
|
||||
Body: buf,
|
||||
Context: ctx,
|
||||
Method: http.MethodPost,
|
||||
OKStatus: http.StatusAccepted,
|
||||
Out: &resp,
|
||||
URL: "https://api.twitch.tv/helix/eventsub/subscriptions",
|
||||
}); err != nil {
|
||||
return nil, errors.Wrap(err, "executing request")
|
||||
}
|
||||
|
||||
return &resp.Data[0], nil
|
||||
}
|
||||
|
||||
func (c *Client) deleteEventSubSubscription(ctx context.Context, id string) error {
|
||||
return errors.Wrap(c.request(clientRequestOpts{
|
||||
AuthType: authTypeAppAccessToken,
|
||||
Context: ctx,
|
||||
Method: http.MethodDelete,
|
||||
OKStatus: http.StatusNoContent,
|
||||
URL: fmt.Sprintf("https://api.twitch.tv/helix/eventsub/subscriptions?id=%s", id),
|
||||
}), "executing request")
|
||||
}
|
||||
|
||||
func (c *Client) getEventSubSubscriptions(ctx context.Context) ([]eventSubSubscription, error) {
|
||||
var (
|
||||
out []eventSubSubscription
|
||||
params = make(url.Values)
|
||||
resp struct {
|
||||
Total int64 `json:"total"`
|
||||
Data []eventSubSubscription `json:"data"`
|
||||
Pagination struct {
|
||||
Cursor string `json:"cursor"`
|
||||
} `json:"pagination"`
|
||||
}
|
||||
)
|
||||
|
||||
for {
|
||||
if err := c.request(clientRequestOpts{
|
||||
AuthType: authTypeAppAccessToken,
|
||||
Context: ctx,
|
||||
Method: http.MethodGet,
|
||||
OKStatus: http.StatusOK,
|
||||
Out: &resp,
|
||||
URL: fmt.Sprintf("https://api.twitch.tv/helix/eventsub/subscriptions?%s", params.Encode()),
|
||||
}); err != nil {
|
||||
return nil, errors.Wrap(err, "executing request")
|
||||
}
|
||||
|
||||
out = append(out, resp.Data...)
|
||||
|
||||
if resp.Pagination.Cursor == "" {
|
||||
break
|
||||
}
|
||||
|
||||
params.Set("after", resp.Pagination.Cursor)
|
||||
resp.Pagination.Cursor = "" // Clear from struct as struct is reused
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *Client) getTwitchAppAccessToken() (string, error) {
|
||||
if c.appAccessToken != "" {
|
||||
return c.appAccessToken, nil
|
||||
|
|
187
pkg/twitch/users.go
Normal file
187
pkg/twitch/users.go
Normal file
|
@ -0,0 +1,187 @@
|
|||
package twitch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type (
|
||||
User struct {
|
||||
DisplayName string `json:"display_name"`
|
||||
ID string `json:"id"`
|
||||
Login string `json:"login"`
|
||||
ProfileImageURL string `json:"profile_image_url"`
|
||||
}
|
||||
)
|
||||
|
||||
func (c *Client) GetAuthorizedUser() (userID string, userName string, err error) {
|
||||
var payload struct {
|
||||
Data []User `json:"data"`
|
||||
}
|
||||
|
||||
if err := c.request(clientRequestOpts{
|
||||
AuthType: authTypeBearerToken,
|
||||
Context: context.Background(),
|
||||
Method: http.MethodGet,
|
||||
OKStatus: http.StatusOK,
|
||||
Out: &payload,
|
||||
URL: "https://api.twitch.tv/helix/users",
|
||||
}); err != nil {
|
||||
return "", "", errors.Wrap(err, "request channel info")
|
||||
}
|
||||
|
||||
if l := len(payload.Data); l != 1 {
|
||||
return "", "", errors.Errorf("unexpected number of users returned: %d", l)
|
||||
}
|
||||
|
||||
return payload.Data[0].ID, payload.Data[0].Login, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetDisplayNameForUser(username string) (string, error) {
|
||||
cacheKey := []string{"displayNameForUsername", username}
|
||||
if d := c.apiCache.Get(cacheKey); d != nil {
|
||||
return d.(string), nil
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
Data []User `json:"data"`
|
||||
}
|
||||
|
||||
if err := c.request(clientRequestOpts{
|
||||
AuthType: authTypeAppAccessToken,
|
||||
Context: context.Background(),
|
||||
Method: http.MethodGet,
|
||||
Out: &payload,
|
||||
URL: fmt.Sprintf("https://api.twitch.tv/helix/users?login=%s", username),
|
||||
}); err != nil {
|
||||
return "", errors.Wrap(err, "request channel info")
|
||||
}
|
||||
|
||||
if l := len(payload.Data); l != 1 {
|
||||
return "", errors.Errorf("unexpected number of users returned: %d", l)
|
||||
}
|
||||
|
||||
// The DisplayName for an username will not change (often), cache for a decent time
|
||||
c.apiCache.Set(cacheKey, time.Hour, payload.Data[0].DisplayName)
|
||||
|
||||
return payload.Data[0].DisplayName, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetFollowDate(from, to string) (time.Time, error) {
|
||||
cacheKey := []string{"followDate", from, to}
|
||||
if d := c.apiCache.Get(cacheKey); d != nil {
|
||||
return d.(time.Time), nil
|
||||
}
|
||||
|
||||
fromID, err := c.GetIDForUsername(from)
|
||||
if err != nil {
|
||||
return time.Time{}, errors.Wrap(err, "getting id for 'from' user")
|
||||
}
|
||||
toID, err := c.GetIDForUsername(to)
|
||||
if err != nil {
|
||||
return time.Time{}, errors.Wrap(err, "getting id for 'to' user")
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
Data []struct {
|
||||
FollowedAt time.Time `json:"followed_at"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
if err := c.request(clientRequestOpts{
|
||||
AuthType: authTypeAppAccessToken,
|
||||
Context: context.Background(),
|
||||
Method: http.MethodGet,
|
||||
OKStatus: http.StatusOK,
|
||||
Out: &payload,
|
||||
URL: fmt.Sprintf("https://api.twitch.tv/helix/users/follows?to_id=%s&from_id=%s", toID, fromID),
|
||||
}); err != nil {
|
||||
return time.Time{}, errors.Wrap(err, "request follow info")
|
||||
}
|
||||
|
||||
if l := len(payload.Data); l != 1 {
|
||||
return time.Time{}, errors.Errorf("unexpected number of records returned: %d", l)
|
||||
}
|
||||
|
||||
// Follow date will not change that often, cache for a long time
|
||||
c.apiCache.Set(cacheKey, timeDay, payload.Data[0].FollowedAt)
|
||||
|
||||
return payload.Data[0].FollowedAt, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetIDForUsername(username string) (string, error) {
|
||||
cacheKey := []string{"idForUsername", username}
|
||||
if d := c.apiCache.Get(cacheKey); d != nil {
|
||||
return d.(string), nil
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
Data []User `json:"data"`
|
||||
}
|
||||
|
||||
if err := c.request(clientRequestOpts{
|
||||
AuthType: authTypeAppAccessToken,
|
||||
Context: context.Background(),
|
||||
Method: http.MethodGet,
|
||||
OKStatus: http.StatusOK,
|
||||
Out: &payload,
|
||||
URL: fmt.Sprintf("https://api.twitch.tv/helix/users?login=%s", username),
|
||||
}); err != nil {
|
||||
return "", errors.Wrap(err, "request channel info")
|
||||
}
|
||||
|
||||
if l := len(payload.Data); l != 1 {
|
||||
return "", errors.Errorf("unexpected number of users returned: %d", l)
|
||||
}
|
||||
|
||||
// The ID for an username will not change (often), cache for a long time
|
||||
c.apiCache.Set(cacheKey, timeDay, payload.Data[0].ID)
|
||||
|
||||
return payload.Data[0].ID, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetUserInformation(user string) (*User, error) {
|
||||
var (
|
||||
out User
|
||||
param = "login"
|
||||
payload struct {
|
||||
Data []User `json:"data"`
|
||||
}
|
||||
)
|
||||
|
||||
cacheKey := []string{"userInformation", user}
|
||||
if d := c.apiCache.Get(cacheKey); d != nil {
|
||||
out = d.(User)
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
if _, err := strconv.ParseInt(user, 10, 64); err == nil {
|
||||
param = "id"
|
||||
}
|
||||
|
||||
if err := c.request(clientRequestOpts{
|
||||
AuthType: authTypeAppAccessToken,
|
||||
Context: context.Background(),
|
||||
Method: http.MethodGet,
|
||||
OKStatus: http.StatusOK,
|
||||
Out: &payload,
|
||||
URL: fmt.Sprintf("https://api.twitch.tv/helix/users?%s=%s", param, user),
|
||||
}); err != nil {
|
||||
return nil, errors.Wrap(err, "request user info")
|
||||
}
|
||||
|
||||
if l := len(payload.Data); l != 1 {
|
||||
return nil, errors.Errorf("unexpected number of records returned: %d", l)
|
||||
}
|
||||
|
||||
// Follow date will not change that often, cache for a long time
|
||||
c.apiCache.Set(cacheKey, timeDay, payload.Data[0])
|
||||
out = payload.Data[0]
|
||||
|
||||
return &out, nil
|
||||
}
|
53
pkg/twitch/whispers.go
Normal file
53
pkg/twitch/whispers.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package twitch
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// SendWhisper sends a whisper from the bot to the specified user.
|
||||
//
|
||||
// For details about message limits see the official documentation:
|
||||
// https://dev.twitch.tv/docs/api/reference#send-whisper
|
||||
func (c *Client) SendWhisper(toUser, message string) error {
|
||||
var payload struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
payload.Message = message
|
||||
|
||||
botID, _, err := c.GetAuthorizedUser()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "getting bot user-id")
|
||||
}
|
||||
|
||||
targetID, err := c.GetIDForUsername(toUser)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "getting target user-id")
|
||||
}
|
||||
|
||||
body := new(bytes.Buffer)
|
||||
if err = json.NewEncoder(body).Encode(payload); err != nil {
|
||||
return errors.Wrap(err, "encoding payload")
|
||||
}
|
||||
|
||||
return errors.Wrap(
|
||||
c.request(clientRequestOpts{
|
||||
AuthType: authTypeBearerToken,
|
||||
Context: context.Background(),
|
||||
Method: http.MethodPost,
|
||||
OKStatus: http.StatusNoContent,
|
||||
Body: body,
|
||||
URL: fmt.Sprintf(
|
||||
"https://api.twitch.tv/helix/whispers?from_user_id=%s&to_user_id=%s",
|
||||
botID, targetID,
|
||||
),
|
||||
}),
|
||||
"executing whisper request",
|
||||
)
|
||||
}
|
|
@ -2,6 +2,7 @@ package plugins
|
|||
|
||||
import (
|
||||
"github.com/go-irc/irc"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/robfig/cron/v3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
|
@ -41,6 +42,9 @@ type (
|
|||
|
||||
MsgFormatter func(tplString string, m *irc.Message, r *Rule, fields *FieldCollection) (string, error)
|
||||
|
||||
MsgModificationFunc func(*irc.Message) error
|
||||
MsgModificationRegistrationFunc func(linePrefix string, modFn MsgModificationFunc)
|
||||
|
||||
RawMessageHandlerFunc func(m *irc.Message) error
|
||||
RawMessageHandlerRegisterFunc func(RawMessageHandlerFunc) error
|
||||
|
||||
|
@ -70,6 +74,8 @@ type (
|
|||
RegisterCron CronRegistrationFunc
|
||||
// RegisterEventHandler is a method to register a handler function receiving ALL events
|
||||
RegisterEventHandler EventHandlerRegisterFunc
|
||||
// RegisterMessageModFunc is a method to register a handler to modify / react on messages
|
||||
RegisterMessageModFunc MsgModificationRegistrationFunc
|
||||
// RegisterRawMessageHandler is a method to register an handler to receive ALL messages received
|
||||
RegisterRawMessageHandler RawMessageHandlerRegisterFunc
|
||||
// RegisterTemplateFunction can be used to register a new template functions
|
||||
|
@ -102,6 +108,8 @@ type (
|
|||
ValidateTokenFunc func(token string, modules ...string) error
|
||||
)
|
||||
|
||||
var ErrSkipSendingMessage = errors.New("skip sending message")
|
||||
|
||||
func GenericTemplateFunctionGetter(f interface{}) TemplateFuncGetter {
|
||||
return func(*irc.Message, *Rule, *FieldCollection) interface{} { return f }
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
|
||||
"github.com/Luzifer/go_helpers/v2/backoff"
|
||||
"github.com/Luzifer/go_helpers/v2/str"
|
||||
"github.com/Luzifer/twitch-bot/v2/internal/actors/announce"
|
||||
"github.com/Luzifer/twitch-bot/v2/internal/actors/ban"
|
||||
"github.com/Luzifer/twitch-bot/v2/internal/actors/counter"
|
||||
"github.com/Luzifer/twitch-bot/v2/internal/actors/delay"
|
||||
|
@ -42,6 +43,7 @@ const ircHandleWaitRetries = 10
|
|||
var (
|
||||
corePluginRegistrations = []plugins.RegisterFunc{
|
||||
// Actors
|
||||
announce.Register,
|
||||
ban.Register,
|
||||
counter.Register,
|
||||
delay.Register,
|
||||
|
@ -129,6 +131,7 @@ func getRegistrationArguments() plugins.RegistrationArguments {
|
|||
RegisterAPIRoute: registerRoute,
|
||||
RegisterCron: cronService.AddFunc,
|
||||
RegisterEventHandler: registerEventHandlers,
|
||||
RegisterMessageModFunc: registerChatcommand,
|
||||
RegisterRawMessageHandler: registerRawMessageHandler,
|
||||
RegisterTemplateFunction: tplFuncs.Register,
|
||||
SendMessage: sendMessage,
|
||||
|
@ -144,7 +147,22 @@ func getRegistrationArguments() plugins.RegistrationArguments {
|
|||
}
|
||||
|
||||
func sendMessage(m *irc.Message) error {
|
||||
if err := backoff.NewBackoff().WithMaxIterations(ircHandleWaitRetries).Retry(func() error {
|
||||
err := handleChatcommandModifications(m)
|
||||
switch {
|
||||
case err == nil:
|
||||
// There was no error, the message should be sent normally
|
||||
|
||||
case errors.Is(err, plugins.ErrSkipSendingMessage):
|
||||
// One chatcommand handler cancelled sending the message
|
||||
// (probably because it was handled otherwise)
|
||||
return nil
|
||||
|
||||
default:
|
||||
// Something in a chatcommand handler went wrong
|
||||
return errors.Wrap(err, "handling chat commands")
|
||||
}
|
||||
|
||||
if err = backoff.NewBackoff().WithMaxIterations(ircHandleWaitRetries).Retry(func() error {
|
||||
if ircHdl == nil {
|
||||
return errors.New("irc handle not available")
|
||||
}
|
||||
|
|
|
@ -7,13 +7,16 @@ var (
|
|||
twitch.ScopeChannelEditCommercial,
|
||||
twitch.ScopeChannelManageBroadcast,
|
||||
twitch.ScopeChannelReadRedemptions,
|
||||
twitch.ScopeChannelManageRaids,
|
||||
}
|
||||
|
||||
botDefaultScopes = append(channelDefaultScopes,
|
||||
twitch.ScopeChatRead,
|
||||
twitch.ScopeChatEdit,
|
||||
twitch.ScopeChatRead,
|
||||
twitch.ScopeModeratorManageAnnoucements,
|
||||
twitch.ScopeModeratorManageBannedUsers,
|
||||
twitch.ScopeModeratorManageChatMessages,
|
||||
twitch.ScopeModeratorManageChatSettings,
|
||||
twitch.ScopeWhisperRead,
|
||||
twitch.ScopeWhisperEdit,
|
||||
twitch.ScopeChannelModerate,
|
||||
)
|
||||
)
|
||||
|
|
|
@ -82,7 +82,7 @@ func handleStatusRequest(w http.ResponseWriter, r *http.Request) {
|
|||
return errors.New("not initialized")
|
||||
}
|
||||
|
||||
_, err := twitchClient.GetAuthorizedUsername()
|
||||
_, _, err := twitchClient.GetAuthorizedUser()
|
||||
return errors.Wrap(err, "fetching username")
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue