twitch-bot/internal/service/access/access.go

191 lines
4.9 KiB
Go

package access
import (
"database/sql"
"strings"
"github.com/pkg/errors"
"github.com/Luzifer/go_helpers/v2/str"
"github.com/Luzifer/twitch-bot/pkg/database"
"github.com/Luzifer/twitch-bot/pkg/twitch"
)
const (
coreMetaKeyBotToken = "bot_access_token"
coreMetaKeyBotRefreshToken = "bot_refresh_token" //#nosec:G101 // That's a key, not a credential
)
type (
ClientConfig struct {
TwitchClient string
TwitchClientSecret string
FallbackToken string
TokenUpdateHook func()
}
Service struct{ db database.Connector }
)
func New(db database.Connector) *Service {
return &Service{db}
}
func (s Service) GetBotTwitchClient(cfg ClientConfig) (*twitch.Client, error) {
var botAccessToken, botRefreshToken string
err := s.db.ReadEncryptedCoreMeta(coreMetaKeyBotToken, &botAccessToken)
switch {
case errors.Is(err, nil):
// This is fine
case errors.Is(err, database.ErrCoreMetaNotFound):
botAccessToken = cfg.FallbackToken
default:
return nil, errors.Wrap(err, "getting bot access token from database")
}
if err = s.db.ReadEncryptedCoreMeta(coreMetaKeyBotToken, &botAccessToken); err != nil && !errors.Is(err, database.ErrCoreMetaNotFound) {
return nil, errors.Wrap(err, "getting bot refresh token from database")
}
twitchClient := twitch.New(cfg.TwitchClient, cfg.TwitchClientSecret, botAccessToken, botRefreshToken)
twitchClient.SetTokenUpdateHook(s.SetBotTwitchCredentials)
return twitchClient, nil
}
func (s Service) GetTwitchClientForChannel(channel string, cfg ClientConfig) (*twitch.Client, error) {
var err error
row := s.db.DB().QueryRow(
`SELECT access_token, refresh_token, scopes
FROM extended_permissions
WHERE channel = $1`,
channel,
)
var accessToken, refreshToken, scopeStr string
if err = row.Scan(&accessToken, &refreshToken, &scopeStr); err != nil {
return nil, errors.Wrap(err, "getting twitch credentials from database")
}
if accessToken, err = s.db.DecryptField(accessToken); err != nil {
return nil, errors.Wrap(err, "decrypting access token")
}
if refreshToken, err = s.db.DecryptField(refreshToken); err != nil {
return nil, errors.Wrap(err, "decrypting refresh token")
}
scopes := strings.Split(scopeStr, " ")
tc := twitch.New(cfg.TwitchClient, cfg.TwitchClientSecret, accessToken, refreshToken)
tc.SetTokenUpdateHook(func(at, rt string) error {
return errors.Wrap(s.SetExtendedTwitchCredentials(channel, at, rt, scopes), "updating extended permissions token")
})
return tc, nil
}
func (s Service) HasAnyPermissionForChannel(channel string, scopes ...string) (bool, error) {
row := s.db.DB().QueryRow(
`SELECT scopes
FROM extended_permissions
WHERE channel = $1`,
channel,
)
var scopeStr string
if err := row.Scan(&scopeStr); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return false, nil
}
return false, errors.Wrap(err, "getting scopes from database")
}
storedScopes := strings.Split(scopeStr, " ")
for _, scope := range scopes {
if str.StringInSlice(scope, storedScopes) {
return true, nil
}
}
return false, nil
}
func (s Service) HasPermissionsForChannel(channel string, scopes ...string) (bool, error) {
row := s.db.DB().QueryRow(
`SELECT scopes
FROM extended_permissions
WHERE channel = $1`,
channel,
)
var scopeStr string
if err := row.Scan(&scopeStr); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return false, nil
}
return false, errors.Wrap(err, "getting scopes from database")
}
storedScopes := strings.Split(scopeStr, " ")
for _, scope := range scopes {
if !str.StringInSlice(scope, storedScopes) {
return false, nil
}
}
return true, nil
}
func (s Service) RemoveExendedTwitchCredentials(channel string) error {
_, err := s.db.DB().Exec(
`DELETE FROM extended_permissions
WHERE channel = $1`,
channel,
)
return errors.Wrap(err, "deleting data from table")
}
func (s Service) SetBotTwitchCredentials(accessToken, refreshToken string) (err error) {
if err = s.db.StoreEncryptedCoreMeta(coreMetaKeyBotToken, accessToken); err != nil {
return errors.Wrap(err, "storing bot access token")
}
if err = s.db.StoreEncryptedCoreMeta(coreMetaKeyBotRefreshToken, refreshToken); err != nil {
return errors.Wrap(err, "storing bot refresh token")
}
return nil
}
func (s Service) SetExtendedTwitchCredentials(channel, accessToken, refreshToken string, scope []string) (err error) {
if accessToken, err = s.db.EncryptField(accessToken); err != nil {
return errors.Wrap(err, "encrypting access token")
}
if refreshToken, err = s.db.EncryptField(refreshToken); err != nil {
return errors.Wrap(err, "encrypting refresh token")
}
_, err = s.db.DB().Exec(
`INSERT INTO extended_permissions
(channel, access_token, refresh_token, scopes)
VALUES ($1, $2, $3, $4)
ON CONFLICT DO UPDATE SET
access_token=excluded.access_token,
refresh_token=excluded.refresh_token,
scopes=excluded.scopes;`,
channel, accessToken, refreshToken, strings.Join(scope, " "),
)
return errors.Wrap(err, "inserting data into table")
}