[#26] Fix: Modify channel module not working for editor-bots (#27)

This commit is contained in:
Knut Ahlers 2022-04-23 17:24:49 +02:00 committed by GitHub
parent 8894d35fa7
commit 5a9e589ff5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 132 additions and 42 deletions

14
auth.go
View File

@ -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
} }

View File

@ -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

View File

@ -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{}) {

View File

@ -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",

View File

@ -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

View File

@ -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,

View File

@ -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,
}
) )

117
store.go
View File

@ -14,27 +14,39 @@ 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")
Counters map[string]int64 `json:"counters"`
Timers map[string]plugins.TimerEntry `json:"timers"`
Variables map[string]string `json:"variables"`
ModuleStorage map[string]json.RawMessage `json:"module_storage"` type (
storageExtendedPermission struct {
AccessToken string `encrypt:"true" json:"access_token,omitempty"`
RefreshToken string `encrypt:"true" json:"refresh_token,omitempty"`
Scopes []string `json:"scopes,omitempty"`
}
GrantedScopes map[string][]string `json:"granted_scopes"` storageFile struct {
Counters map[string]int64 `json:"counters"`
Timers map[string]plugins.TimerEntry `json:"timers"`
Variables map[string]string `json:"variables"`
EventSubSecret string `encrypt:"true" json:"event_sub_secret,omitempty"` ModuleStorage map[string]json.RawMessage `json:"module_storage"`
BotAccessToken string `encrypt:"true" json:"bot_access_token,omitempty"` GrantedScopes map[string][]string `json:"granted_scopes,omitempty"` // Deprecated, Read-Only
BotRefreshToken string `encrypt:"true" json:"bot_refresh_token,omitempty"` ExtendedPermissions map[string]*storageExtendedPermission `json:"extended_permissions"`
inMem bool EventSubSecret string `encrypt:"true" json:"event_sub_secret,omitempty"`
lock *sync.RWMutex
} BotAccessToken string `encrypt:"true" json:"bot_access_token,omitempty"`
BotRefreshToken string `encrypt:"true" json:"bot_refresh_token,omitempty"`
inMem bool
lock *sync.RWMutex
}
)
func newStorageFile(inMemStore bool) *storageFile { func newStorageFile(inMemStore bool) *storageFile {
return &storageFile{ return &storageFile{
@ -44,18 +56,19 @@ 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)
} }
} }
if data.AccessToken == "" && prev.AccessToken != "" {
data.AccessToken = prev.AccessToken
}
if data.RefreshToken == "" && prev.RefreshToken != "" {
data.RefreshToken = prev.RefreshToken
}
} }
s.GrantedScopes[user] = scopes 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
}

View File

@ -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",
) )
} }