mirror of
https://github.com/Luzifer/twitch-bot.git
synced 2024-11-09 16:50:01 +00:00
parent
8894d35fa7
commit
5a9e589ff5
9 changed files with 132 additions and 42 deletions
14
auth.go
14
auth.go
|
@ -92,7 +92,11 @@ func handleAuthUpdateBotToken(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
twitchClient.UpdateToken(rData.AccessToken, rData.RefreshToken)
|
twitchClient.UpdateToken(rData.AccessToken, rData.RefreshToken)
|
||||||
|
|
||||||
if err = store.SetGrantedScopes(botUser, rData.Scope, true); err != nil {
|
if err = store.SetExtendedPermissions(botUser, storageExtendedPermission{
|
||||||
|
AccessToken: rData.AccessToken,
|
||||||
|
RefreshToken: rData.RefreshToken,
|
||||||
|
Scopes: rData.Scope,
|
||||||
|
}, true); err != nil {
|
||||||
http.Error(w, errors.Wrap(err, "storing access scopes").Error(), http.StatusInternalServerError)
|
http.Error(w, errors.Wrap(err, "storing access scopes").Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -141,8 +145,12 @@ func handleAuthUpdateChannelGrant(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = store.SetGrantedScopes(grantUser, rData.Scope, false); err != nil {
|
if err = store.SetExtendedPermissions(grantUser, storageExtendedPermission{
|
||||||
http.Error(w, errors.Wrap(err, "storing access scopes").Error(), http.StatusInternalServerError)
|
AccessToken: rData.AccessToken,
|
||||||
|
RefreshToken: rData.RefreshToken,
|
||||||
|
Scopes: rData.Scope,
|
||||||
|
}, false); err != nil {
|
||||||
|
http.Error(w, errors.Wrap(err, "storing access token").Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -186,7 +186,7 @@ func configEditorHandleGeneralGet(w http.ResponseWriter, r *http.Request) {
|
||||||
elevated := make(map[string]bool)
|
elevated := make(map[string]bool)
|
||||||
|
|
||||||
for _, ch := range config.Channels {
|
for _, ch := range config.Channels {
|
||||||
elevated[ch] = store.UserHasGrantedScopes(ch, channelDefaultScopes...)
|
elevated[ch] = store.UserHasGrantedScopes(ch, channelDefaultScopes...) && store.UserHasExtendedAuth(ch)
|
||||||
}
|
}
|
||||||
|
|
||||||
var uName *string
|
var uName *string
|
||||||
|
|
|
@ -65,6 +65,16 @@ func handleEncryptedTags(obj interface{}, passphrase string, action encryptActio
|
||||||
hasEncryption := t.Tag.Get("encrypt") == "true"
|
hasEncryption := t.Tag.Get("encrypt") == "true"
|
||||||
|
|
||||||
switch t.Type.Kind() {
|
switch t.Type.Kind() {
|
||||||
|
// Type: Map - see whether value is struct
|
||||||
|
case reflect.Map:
|
||||||
|
if t.Type.Elem().Kind() == reflect.Ptr && t.Type.Elem().Elem().Kind() == reflect.Struct {
|
||||||
|
for _, k := range v.MapKeys() {
|
||||||
|
if err := handleEncryptedTags(v.MapIndex(k).Interface(), passphrase, action); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Type: Pointer - Recurse if not nil and struct inside
|
// Type: Pointer - Recurse if not nil and struct inside
|
||||||
case reflect.Ptr:
|
case reflect.Ptr:
|
||||||
if !v.IsNil() && v.Elem().Kind() == reflect.Struct && t.Type != reflect.TypeOf(&time.Time{}) {
|
if !v.IsNil() && v.Elem().Kind() == reflect.Struct && t.Type != reflect.TypeOf(&time.Time{}) {
|
||||||
|
|
|
@ -15,12 +15,12 @@ const actorName = "modchannel"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
formatMessage plugins.MsgFormatter
|
formatMessage plugins.MsgFormatter
|
||||||
twitchClient *twitch.Client
|
tcGetter func(string) (*twitch.Client, error)
|
||||||
)
|
)
|
||||||
|
|
||||||
func Register(args plugins.RegistrationArguments) error {
|
func Register(args plugins.RegistrationArguments) error {
|
||||||
formatMessage = args.FormatMessage
|
formatMessage = args.FormatMessage
|
||||||
twitchClient = args.GetTwitchClient()
|
tcGetter = args.GetTwitchClientForChannel
|
||||||
|
|
||||||
args.RegisterActor(actorName, func() plugins.Actor { return &actor{} })
|
args.RegisterActor(actorName, func() plugins.Actor { return &actor{} })
|
||||||
|
|
||||||
|
@ -101,6 +101,11 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
|
||||||
updTitle = &parsedTitle
|
updTitle = &parsedTitle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
twitchClient, err := tcGetter(strings.TrimLeft(channel, "#"))
|
||||||
|
if err != nil {
|
||||||
|
return false, errors.Wrap(err, "getting Twitch client")
|
||||||
|
}
|
||||||
|
|
||||||
return false, errors.Wrap(
|
return false, errors.Wrap(
|
||||||
twitchClient.ModifyChannelInformation(context.Background(), strings.TrimLeft(channel, "#"), updGame, updTitle),
|
twitchClient.ModifyChannelInformation(context.Background(), strings.TrimLeft(channel, "#"), updGame, updTitle),
|
||||||
"updating channel info",
|
"updating channel info",
|
||||||
|
|
|
@ -57,6 +57,8 @@ type (
|
||||||
GetStorageManager func() StorageManager
|
GetStorageManager func() StorageManager
|
||||||
// GetTwitchClient retrieves a fully configured Twitch client with initialized cache
|
// GetTwitchClient retrieves a fully configured Twitch client with initialized cache
|
||||||
GetTwitchClient func() *twitch.Client
|
GetTwitchClient func() *twitch.Client
|
||||||
|
// GetTwitchClientForChannel retrieves a fully configured Twitch client with initialized cache for extended permission channels
|
||||||
|
GetTwitchClientForChannel func(string) (*twitch.Client, error)
|
||||||
// RegisterActor is used to register a new IRC rule-actor implementing the Actor interface
|
// RegisterActor is used to register a new IRC rule-actor implementing the Actor interface
|
||||||
RegisterActor ActorRegistrationFunc
|
RegisterActor ActorRegistrationFunc
|
||||||
// RegisterActorDocumentation is used to register an ActorDocumentation for the config editor
|
// RegisterActorDocumentation is used to register an ActorDocumentation for the config editor
|
||||||
|
|
|
@ -114,6 +114,7 @@ func getRegistrationArguments() plugins.RegistrationArguments {
|
||||||
GetLogger: func(moduleName string) *log.Entry { return log.WithField("module", moduleName) },
|
GetLogger: func(moduleName string) *log.Entry { return log.WithField("module", moduleName) },
|
||||||
GetStorageManager: func() plugins.StorageManager { return store },
|
GetStorageManager: func() plugins.StorageManager { return store },
|
||||||
GetTwitchClient: func() *twitch.Client { return twitchClient },
|
GetTwitchClient: func() *twitch.Client { return twitchClient },
|
||||||
|
GetTwitchClientForChannel: store.GetTwitchClientForChannel,
|
||||||
RegisterActor: registerAction,
|
RegisterActor: registerAction,
|
||||||
RegisterActorDocumentation: registerActorDocumentation,
|
RegisterActorDocumentation: registerActorDocumentation,
|
||||||
RegisterAPIRoute: registerRoute,
|
RegisterAPIRoute: registerRoute,
|
||||||
|
|
17
scopes.go
17
scopes.go
|
@ -3,18 +3,17 @@ package main
|
||||||
import "github.com/Luzifer/twitch-bot/twitch"
|
import "github.com/Luzifer/twitch-bot/twitch"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
botDefaultScopes = []string{
|
channelDefaultScopes = []string{
|
||||||
|
twitch.ScopeChannelEditCommercial,
|
||||||
|
twitch.ScopeChannelManageBroadcast,
|
||||||
|
twitch.ScopeChannelReadRedemptions,
|
||||||
|
}
|
||||||
|
|
||||||
|
botDefaultScopes = append(channelDefaultScopes,
|
||||||
twitch.ScopeChatRead,
|
twitch.ScopeChatRead,
|
||||||
twitch.ScopeChatEdit,
|
twitch.ScopeChatEdit,
|
||||||
twitch.ScopeWhisperRead,
|
twitch.ScopeWhisperRead,
|
||||||
twitch.ScopeWhisperEdit,
|
twitch.ScopeWhisperEdit,
|
||||||
twitch.ScopeChannelModerate,
|
twitch.ScopeChannelModerate,
|
||||||
twitch.ScopeChannelManageBroadcast,
|
)
|
||||||
twitch.ScopeChannelEditCommercial,
|
|
||||||
twitch.ScopeV5ChannelEditor,
|
|
||||||
}
|
|
||||||
|
|
||||||
channelDefaultScopes = []string{
|
|
||||||
twitch.ScopeChannelReadRedemptions,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
99
store.go
99
store.go
|
@ -14,18 +14,29 @@ import (
|
||||||
"github.com/Luzifer/go_helpers/v2/str"
|
"github.com/Luzifer/go_helpers/v2/str"
|
||||||
"github.com/Luzifer/twitch-bot/crypt"
|
"github.com/Luzifer/twitch-bot/crypt"
|
||||||
"github.com/Luzifer/twitch-bot/plugins"
|
"github.com/Luzifer/twitch-bot/plugins"
|
||||||
|
"github.com/Luzifer/twitch-bot/twitch"
|
||||||
)
|
)
|
||||||
|
|
||||||
const eventSubSecretLength = 32
|
const eventSubSecretLength = 32
|
||||||
|
|
||||||
type storageFile struct {
|
var errExtendedPermissionsMissing = errors.New("no extended permissions greanted")
|
||||||
|
|
||||||
|
type (
|
||||||
|
storageExtendedPermission struct {
|
||||||
|
AccessToken string `encrypt:"true" json:"access_token,omitempty"`
|
||||||
|
RefreshToken string `encrypt:"true" json:"refresh_token,omitempty"`
|
||||||
|
Scopes []string `json:"scopes,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
storageFile struct {
|
||||||
Counters map[string]int64 `json:"counters"`
|
Counters map[string]int64 `json:"counters"`
|
||||||
Timers map[string]plugins.TimerEntry `json:"timers"`
|
Timers map[string]plugins.TimerEntry `json:"timers"`
|
||||||
Variables map[string]string `json:"variables"`
|
Variables map[string]string `json:"variables"`
|
||||||
|
|
||||||
ModuleStorage map[string]json.RawMessage `json:"module_storage"`
|
ModuleStorage map[string]json.RawMessage `json:"module_storage"`
|
||||||
|
|
||||||
GrantedScopes map[string][]string `json:"granted_scopes"`
|
GrantedScopes map[string][]string `json:"granted_scopes,omitempty"` // Deprecated, Read-Only
|
||||||
|
ExtendedPermissions map[string]*storageExtendedPermission `json:"extended_permissions"`
|
||||||
|
|
||||||
EventSubSecret string `encrypt:"true" json:"event_sub_secret,omitempty"`
|
EventSubSecret string `encrypt:"true" json:"event_sub_secret,omitempty"`
|
||||||
|
|
||||||
|
@ -34,7 +45,8 @@ type storageFile struct {
|
||||||
|
|
||||||
inMem bool
|
inMem bool
|
||||||
lock *sync.RWMutex
|
lock *sync.RWMutex
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
func newStorageFile(inMemStore bool) *storageFile {
|
func newStorageFile(inMemStore bool) *storageFile {
|
||||||
return &storageFile{
|
return &storageFile{
|
||||||
|
@ -45,17 +57,18 @@ func newStorageFile(inMemStore bool) *storageFile {
|
||||||
ModuleStorage: map[string]json.RawMessage{},
|
ModuleStorage: map[string]json.RawMessage{},
|
||||||
|
|
||||||
GrantedScopes: map[string][]string{},
|
GrantedScopes: map[string][]string{},
|
||||||
|
ExtendedPermissions: map[string]*storageExtendedPermission{},
|
||||||
|
|
||||||
inMem: inMemStore,
|
inMem: inMemStore,
|
||||||
lock: new(sync.RWMutex),
|
lock: new(sync.RWMutex),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *storageFile) DeleteGrantedScopes(user string) error {
|
func (s *storageFile) DeleteExtendedPermissions(user string) error {
|
||||||
s.lock.Lock()
|
s.lock.Lock()
|
||||||
defer s.lock.Unlock()
|
defer s.lock.Unlock()
|
||||||
|
|
||||||
delete(s.GrantedScopes, user)
|
delete(s.ExtendedPermissions, user)
|
||||||
|
|
||||||
return errors.Wrap(s.Save(), "saving store")
|
return errors.Wrap(s.Save(), "saving store")
|
||||||
}
|
}
|
||||||
|
@ -118,6 +131,26 @@ func (s *storageFile) GetModuleStore(moduleUUID string, storedObject plugins.Sto
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *storageFile) GetTwitchClientForChannel(channel string) (*twitch.Client, error) {
|
||||||
|
s.lock.RLock()
|
||||||
|
defer s.lock.RUnlock()
|
||||||
|
|
||||||
|
perms := s.ExtendedPermissions[channel]
|
||||||
|
if perms == nil {
|
||||||
|
return nil, errExtendedPermissionsMissing
|
||||||
|
}
|
||||||
|
|
||||||
|
tc := twitch.New(cfg.TwitchClient, cfg.TwitchClientSecret, perms.AccessToken, perms.RefreshToken)
|
||||||
|
tc.SetTokenUpdateHook(func(at, rt string) error {
|
||||||
|
return errors.Wrap(s.SetExtendedPermissions(channel, storageExtendedPermission{
|
||||||
|
AccessToken: at,
|
||||||
|
RefreshToken: rt,
|
||||||
|
}, true), "updating extended permissions token")
|
||||||
|
})
|
||||||
|
|
||||||
|
return tc, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *storageFile) GetVariable(key string) string {
|
func (s *storageFile) GetVariable(key string) string {
|
||||||
s.lock.RLock()
|
s.lock.RLock()
|
||||||
defer s.lock.RUnlock()
|
defer s.lock.RUnlock()
|
||||||
|
@ -166,7 +199,7 @@ func (s *storageFile) Load() error {
|
||||||
return errors.Wrap(err, "decrypting storage object")
|
return errors.Wrap(err, "decrypting storage object")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return errors.Wrap(s.migrate(), "migrating storage")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *storageFile) Save() error {
|
func (s *storageFile) Save() error {
|
||||||
|
@ -218,19 +251,28 @@ func (s *storageFile) Save() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *storageFile) SetGrantedScopes(user string, scopes []string, merge bool) error {
|
func (s *storageFile) SetExtendedPermissions(user string, data storageExtendedPermission, merge bool) error {
|
||||||
s.lock.Lock()
|
s.lock.Lock()
|
||||||
defer s.lock.Unlock()
|
defer s.lock.Unlock()
|
||||||
|
|
||||||
if merge {
|
prev := s.ExtendedPermissions[user]
|
||||||
for _, sc := range s.GrantedScopes[user] {
|
if merge && prev != nil {
|
||||||
if !str.StringInSlice(sc, scopes) {
|
for _, sc := range prev.Scopes {
|
||||||
scopes = append(scopes, sc)
|
if !str.StringInSlice(sc, data.Scopes) {
|
||||||
}
|
data.Scopes = append(data.Scopes, sc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
s.GrantedScopes[user] = scopes
|
if data.AccessToken == "" && prev.AccessToken != "" {
|
||||||
|
data.AccessToken = prev.AccessToken
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.RefreshToken == "" && prev.RefreshToken != "" {
|
||||||
|
data.RefreshToken = prev.RefreshToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.ExtendedPermissions[user] = &data
|
||||||
|
|
||||||
return errors.Wrap(s.Save(), "saving store")
|
return errors.Wrap(s.Save(), "saving store")
|
||||||
}
|
}
|
||||||
|
@ -303,15 +345,23 @@ func (s *storageFile) UpdateCounter(counter string, value int64, absolute bool)
|
||||||
return errors.Wrap(s.Save(), "saving store")
|
return errors.Wrap(s.Save(), "saving store")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *storageFile) UserHasExtendedAuth(user string) bool {
|
||||||
|
s.lock.RLock()
|
||||||
|
defer s.lock.RUnlock()
|
||||||
|
|
||||||
|
ep := s.ExtendedPermissions[user]
|
||||||
|
return ep != nil && ep.AccessToken != "" && ep.RefreshToken != ""
|
||||||
|
}
|
||||||
|
|
||||||
func (s *storageFile) UserHasGrantedAnyScope(user string, scopes ...string) bool {
|
func (s *storageFile) UserHasGrantedAnyScope(user string, scopes ...string) bool {
|
||||||
s.lock.RLock()
|
s.lock.RLock()
|
||||||
defer s.lock.RUnlock()
|
defer s.lock.RUnlock()
|
||||||
|
|
||||||
grantedScopes, ok := s.GrantedScopes[user]
|
if s.ExtendedPermissions[user] == nil {
|
||||||
if !ok {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
grantedScopes := s.ExtendedPermissions[user].Scopes
|
||||||
for _, scope := range scopes {
|
for _, scope := range scopes {
|
||||||
if str.StringInSlice(scope, grantedScopes) {
|
if str.StringInSlice(scope, grantedScopes) {
|
||||||
return true
|
return true
|
||||||
|
@ -325,11 +375,11 @@ func (s *storageFile) UserHasGrantedScopes(user string, scopes ...string) bool {
|
||||||
s.lock.RLock()
|
s.lock.RLock()
|
||||||
defer s.lock.RUnlock()
|
defer s.lock.RUnlock()
|
||||||
|
|
||||||
grantedScopes, ok := s.GrantedScopes[user]
|
if s.ExtendedPermissions[user] == nil {
|
||||||
if !ok {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
grantedScopes := s.ExtendedPermissions[user].Scopes
|
||||||
for _, scope := range scopes {
|
for _, scope := range scopes {
|
||||||
if !str.StringInSlice(scope, grantedScopes) {
|
if !str.StringInSlice(scope, grantedScopes) {
|
||||||
return false
|
return false
|
||||||
|
@ -338,3 +388,18 @@ func (s *storageFile) UserHasGrantedScopes(user string, scopes ...string) bool {
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *storageFile) migrate() error {
|
||||||
|
// Do NOT lock, use during locked call
|
||||||
|
|
||||||
|
// Migration: Transform GrantedScopes and delete
|
||||||
|
for ch, scopes := range s.GrantedScopes {
|
||||||
|
if s.ExtendedPermissions[ch] != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.ExtendedPermissions[ch] = &storageExtendedPermission{Scopes: scopes}
|
||||||
|
}
|
||||||
|
s.GrantedScopes = nil
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -171,7 +171,7 @@ func (t *twitchWatcher) handleEventUserAuthRevoke(m json.RawMessage) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.Wrap(
|
return errors.Wrap(
|
||||||
store.DeleteGrantedScopes(payload.UserLogin),
|
store.DeleteExtendedPermissions(payload.UserLogin),
|
||||||
"deleting granted scopes",
|
"deleting granted scopes",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue