[#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)
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)
return
}
@ -141,8 +145,12 @@ func handleAuthUpdateChannelGrant(w http.ResponseWriter, r *http.Request) {
return
}
if err = store.SetGrantedScopes(grantUser, rData.Scope, false); err != nil {
http.Error(w, errors.Wrap(err, "storing access scopes").Error(), http.StatusInternalServerError)
if err = store.SetExtendedPermissions(grantUser, storageExtendedPermission{
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
}

View file

@ -186,7 +186,7 @@ func configEditorHandleGeneralGet(w http.ResponseWriter, r *http.Request) {
elevated := make(map[string]bool)
for _, ch := range config.Channels {
elevated[ch] = store.UserHasGrantedScopes(ch, channelDefaultScopes...)
elevated[ch] = store.UserHasGrantedScopes(ch, channelDefaultScopes...) && store.UserHasExtendedAuth(ch)
}
var uName *string

View file

@ -65,6 +65,16 @@ func handleEncryptedTags(obj interface{}, passphrase string, action encryptActio
hasEncryption := t.Tag.Get("encrypt") == "true"
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
case reflect.Ptr:
if !v.IsNil() && v.Elem().Kind() == reflect.Struct && t.Type != reflect.TypeOf(&time.Time{}) {

View file

@ -15,12 +15,12 @@ const actorName = "modchannel"
var (
formatMessage plugins.MsgFormatter
twitchClient *twitch.Client
tcGetter func(string) (*twitch.Client, error)
)
func Register(args plugins.RegistrationArguments) error {
formatMessage = args.FormatMessage
twitchClient = args.GetTwitchClient()
tcGetter = args.GetTwitchClientForChannel
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
}
twitchClient, err := tcGetter(strings.TrimLeft(channel, "#"))
if err != nil {
return false, errors.Wrap(err, "getting Twitch client")
}
return false, errors.Wrap(
twitchClient.ModifyChannelInformation(context.Background(), strings.TrimLeft(channel, "#"), updGame, updTitle),
"updating channel info",

View file

@ -57,6 +57,8 @@ type (
GetStorageManager func() StorageManager
// GetTwitchClient retrieves a fully configured Twitch client with initialized cache
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 ActorRegistrationFunc
// 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) },
GetStorageManager: func() plugins.StorageManager { return store },
GetTwitchClient: func() *twitch.Client { return twitchClient },
GetTwitchClientForChannel: store.GetTwitchClientForChannel,
RegisterActor: registerAction,
RegisterActorDocumentation: registerActorDocumentation,
RegisterAPIRoute: registerRoute,

View file

@ -3,18 +3,17 @@ package main
import "github.com/Luzifer/twitch-bot/twitch"
var (
botDefaultScopes = []string{
channelDefaultScopes = []string{
twitch.ScopeChannelEditCommercial,
twitch.ScopeChannelManageBroadcast,
twitch.ScopeChannelReadRedemptions,
}
botDefaultScopes = append(channelDefaultScopes,
twitch.ScopeChatRead,
twitch.ScopeChatEdit,
twitch.ScopeWhisperRead,
twitch.ScopeWhisperEdit,
twitch.ScopeChannelModerate,
twitch.ScopeChannelManageBroadcast,
twitch.ScopeChannelEditCommercial,
twitch.ScopeV5ChannelEditor,
}
channelDefaultScopes = []string{
twitch.ScopeChannelReadRedemptions,
}
)
)

View file

@ -14,18 +14,29 @@ import (
"github.com/Luzifer/go_helpers/v2/str"
"github.com/Luzifer/twitch-bot/crypt"
"github.com/Luzifer/twitch-bot/plugins"
"github.com/Luzifer/twitch-bot/twitch"
)
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"`
Timers map[string]plugins.TimerEntry `json:"timers"`
Variables map[string]string `json:"variables"`
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"`
@ -35,6 +46,7 @@ type storageFile struct {
inMem bool
lock *sync.RWMutex
}
)
func newStorageFile(inMemStore bool) *storageFile {
return &storageFile{
@ -45,17 +57,18 @@ func newStorageFile(inMemStore bool) *storageFile {
ModuleStorage: map[string]json.RawMessage{},
GrantedScopes: map[string][]string{},
ExtendedPermissions: map[string]*storageExtendedPermission{},
inMem: inMemStore,
lock: new(sync.RWMutex),
}
}
func (s *storageFile) DeleteGrantedScopes(user string) error {
func (s *storageFile) DeleteExtendedPermissions(user string) error {
s.lock.Lock()
defer s.lock.Unlock()
delete(s.GrantedScopes, user)
delete(s.ExtendedPermissions, user)
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 {
s.lock.RLock()
defer s.lock.RUnlock()
@ -166,7 +199,7 @@ func (s *storageFile) Load() error {
return errors.Wrap(err, "decrypting storage object")
}
return nil
return errors.Wrap(s.migrate(), "migrating storage")
}
func (s *storageFile) Save() error {
@ -218,19 +251,28 @@ func (s *storageFile) Save() error {
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()
defer s.lock.Unlock()
if merge {
for _, sc := range s.GrantedScopes[user] {
if !str.StringInSlice(sc, scopes) {
scopes = append(scopes, sc)
}
prev := s.ExtendedPermissions[user]
if merge && prev != nil {
for _, sc := range prev.Scopes {
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")
}
@ -303,15 +345,23 @@ func (s *storageFile) UpdateCounter(counter string, value int64, absolute bool)
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 {
s.lock.RLock()
defer s.lock.RUnlock()
grantedScopes, ok := s.GrantedScopes[user]
if !ok {
if s.ExtendedPermissions[user] == nil {
return false
}
grantedScopes := s.ExtendedPermissions[user].Scopes
for _, scope := range scopes {
if str.StringInSlice(scope, grantedScopes) {
return true
@ -325,11 +375,11 @@ func (s *storageFile) UserHasGrantedScopes(user string, scopes ...string) bool {
s.lock.RLock()
defer s.lock.RUnlock()
grantedScopes, ok := s.GrantedScopes[user]
if !ok {
if s.ExtendedPermissions[user] == nil {
return false
}
grantedScopes := s.ExtendedPermissions[user].Scopes
for _, scope := range scopes {
if !str.StringInSlice(scope, grantedScopes) {
return false
@ -338,3 +388,18 @@ func (s *storageFile) UserHasGrantedScopes(user string, scopes ...string) bool {
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(
store.DeleteGrantedScopes(payload.UserLogin),
store.DeleteExtendedPermissions(payload.UserLogin),
"deleting granted scopes",
)
}