mirror of
https://github.com/Luzifer/twitch-bot.git
synced 2024-12-20 11:51:17 +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)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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{}) {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
17
scopes.go
17
scopes.go
|
@ -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,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
|
117
store.go
117
store.go
|
@ -14,27 +14,39 @@ 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 {
|
||||
Counters map[string]int64 `json:"counters"`
|
||||
Timers map[string]plugins.TimerEntry `json:"timers"`
|
||||
Variables map[string]string `json:"variables"`
|
||||
var errExtendedPermissionsMissing = errors.New("no extended permissions greanted")
|
||||
|
||||
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"`
|
||||
BotRefreshToken string `encrypt:"true" json:"bot_refresh_token,omitempty"`
|
||||
GrantedScopes map[string][]string `json:"granted_scopes,omitempty"` // Deprecated, Read-Only
|
||||
ExtendedPermissions map[string]*storageExtendedPermission `json:"extended_permissions"`
|
||||
|
||||
inMem bool
|
||||
lock *sync.RWMutex
|
||||
}
|
||||
EventSubSecret string `encrypt:"true" json:"event_sub_secret,omitempty"`
|
||||
|
||||
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 {
|
||||
return &storageFile{
|
||||
|
@ -44,18 +56,19 @@ func newStorageFile(inMemStore bool) *storageFile {
|
|||
|
||||
ModuleStorage: map[string]json.RawMessage{},
|
||||
|
||||
GrantedScopes: map[string][]string{},
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue