Files
streamctl/cmd/streamd/ui/ui.go
2024-10-16 22:58:55 +01:00

250 lines
7.0 KiB
Go

package ui
import (
"context"
"fmt"
"time"
"github.com/facebookincubator/go-belt"
"github.com/facebookincubator/go-belt/tool/logger"
"github.com/xaionaro-go/streamctl/pkg/oauthhandler"
"github.com/xaionaro-go/streamctl/pkg/streamcontrol"
obs "github.com/xaionaro-go/streamctl/pkg/streamcontrol/obs/types"
twitch "github.com/xaionaro-go/streamctl/pkg/streamcontrol/twitch/types"
youtube "github.com/xaionaro-go/streamctl/pkg/streamcontrol/youtube/types"
"github.com/xaionaro-go/streamctl/pkg/streamd/ui"
"github.com/xaionaro-go/streamctl/pkg/xsync"
)
type UI struct {
OpenBrowserFn func(context.Context, string) error
OAuthURLOpenFn func(listenPort uint16, platID streamcontrol.PlatformName, authURL string) bool
Belt *belt.Belt
RestartFn func(context.Context, string)
CodeChMap map[streamcontrol.PlatformName]chan string
CodeChMapLocker xsync.Mutex
SetLoggingLevelFn func(context.Context, logger.Level)
InputTwitchUserInfoFn func(
ctx context.Context,
cfg *streamcontrol.PlatformConfig[twitch.PlatformSpecificConfig, twitch.StreamProfile],
) (bool, error)
InputYouTubeUserInfoFn func(
ctx context.Context,
cfg *streamcontrol.PlatformConfig[youtube.PlatformSpecificConfig, youtube.StreamProfile],
) (bool, error)
InputOBSConnectInfoFn func(
ctx context.Context,
cfg *streamcontrol.PlatformConfig[obs.PlatformSpecificConfig, obs.StreamProfile],
) (bool, error)
}
var _ ui.UI = (*UI)(nil)
func NewUI(
ctx context.Context,
openBrowserFn func(context.Context, string) error,
oauthURLOpener func(listenPort uint16, platID streamcontrol.PlatformName, authURL string) bool,
restartFn func(context.Context, string),
setLoggingLevel func(context.Context, logger.Level),
inputTwitchUserInfoFn func(
ctx context.Context,
cfg *streamcontrol.PlatformConfig[twitch.PlatformSpecificConfig, twitch.StreamProfile],
) (bool, error),
inputYouTubeUserInfoFn func(
ctx context.Context,
cfg *streamcontrol.PlatformConfig[youtube.PlatformSpecificConfig, youtube.StreamProfile],
) (bool, error),
inputOBSConnectInfoFn func(
ctx context.Context,
cfg *streamcontrol.PlatformConfig[obs.PlatformSpecificConfig, obs.StreamProfile],
) (bool, error),
) *UI {
return &UI{
OpenBrowserFn: openBrowserFn,
OAuthURLOpenFn: oauthURLOpener,
Belt: belt.CtxBelt(ctx),
RestartFn: restartFn,
CodeChMap: map[streamcontrol.PlatformName]chan string{},
CodeChMapLocker: xsync.RWMutex{},
SetLoggingLevelFn: setLoggingLevel,
InputTwitchUserInfoFn: inputTwitchUserInfoFn,
InputYouTubeUserInfoFn: inputYouTubeUserInfoFn,
InputOBSConnectInfoFn: inputOBSConnectInfoFn,
}
}
func (ui *UI) OpenBrowser(ctx context.Context, url string) error {
logger.Debugf(ctx, "UI.OpenBrowser(ctx, '%s')", url)
defer logger.Debugf(ctx, "/UI.OpenBrowser(ctx, '%s')", url)
return ui.OpenBrowserFn(ctx, url)
}
func (ui *UI) SetLoggingLevel(ctx context.Context, level logger.Level) {
ui.SetLoggingLevelFn(ctx, level)
}
func (ui *UI) SetStatus(msg string) {
logger.FromBelt(ui.Belt).Infof("status: %s", msg)
}
func (ui *UI) DisplayError(err error) {
logger.FromBelt(ui.Belt).Errorf("error: %v", err)
}
func (ui *UI) Restart(ctx context.Context, msg string) {
ui.RestartFn(ctx, msg)
}
func (*UI) InputGitUserData(
ctx context.Context,
) (bool, string, []byte, error) {
return false, "", nil, nil
}
func (ui *UI) newOAuthCodeReceiver(
ctx context.Context,
platID streamcontrol.PlatformName,
) (<-chan string, context.CancelFunc) {
return xsync.DoR2(ctx, &ui.CodeChMapLocker, func() (<-chan string, context.CancelFunc) {
return ui.newOAuthCodeReceiverNoLock(ctx, platID)
})
}
func (ui *UI) newOAuthCodeReceiverNoLock(
ctx context.Context,
platID streamcontrol.PlatformName,
) (<-chan string, context.CancelFunc) {
if oldCh, ok := ui.CodeChMap[platID]; ok {
return oldCh, nil
}
ch := make(chan string)
ui.CodeChMap[platID] = ch
return ch, func() {
ui.CodeChMapLocker.Do(ctx, func() {
delete(ui.CodeChMap, platID)
})
}
}
func (ui *UI) getOAuthCodeReceiver(
ctx context.Context,
platID streamcontrol.PlatformName,
) chan<- string {
return xsync.DoR1(ctx, &ui.CodeChMapLocker, func() chan<- string {
return ui.CodeChMap[platID]
})
}
func (ui *UI) oauth2Handler(
ctx context.Context,
platID streamcontrol.PlatformName,
arg oauthhandler.OAuthHandlerArgument,
) error {
logger.Debugf(ctx, "oauth2Handler(ctx, '%s', %#+v)", platID, arg)
defer logger.Debugf(ctx, "/oauth2Handler(ctx, '%s', %#+v)", platID, arg)
ctx, cancelFn := context.WithCancel(ctx)
defer func() {
logger.Debugf(ctx, "cancelling the context")
cancelFn()
}()
codeCh, removeReceiver := ui.newOAuthCodeReceiver(ctx, platID)
if codeCh == nil {
return fmt.Errorf("there is already another oauth handler for this platform running")
}
if removeReceiver != nil {
defer removeReceiver()
}
logger.Debugf(
ctx,
"asking to open the URL '%s' using listen port %d for platform '%s'",
arg.AuthURL,
arg.ListenPort,
platID,
)
ui.OAuthURLOpenFn(arg.ListenPort, platID, arg.AuthURL)
t := time.NewTicker(time.Hour)
defer t.Stop()
for {
logger.Debugf(ctx, "waiting for an auth code")
select {
case <-ctx.Done():
return fmt.Errorf("oauth2Handler is cancelled: %w", ctx.Err())
case code, ok := <-codeCh:
if !ok {
return fmt.Errorf("internal error: codeCh is closed in oauth2Handler")
}
if code == "" {
return fmt.Errorf("internal error: code is empty in oauth2Handler")
}
err := arg.ExchangeFn(code)
if err != nil {
return fmt.Errorf("ExchangeFn returned an error: %w", err)
}
return nil
case <-t.C:
logger.Debugf(ctx, "re-asking to open the URL: %s", arg.AuthURL)
ui.OAuthURLOpenFn(arg.ListenPort, platID, arg.AuthURL)
}
}
}
func (ui *UI) OnSubmittedOAuthCode(
ctx context.Context,
platID streamcontrol.PlatformName,
code string,
) error {
if code == "" {
return fmt.Errorf("code is empty")
}
codeCh := ui.getOAuthCodeReceiver(ctx, platID)
if codeCh == nil {
logger.Debugf(ctx, "no code receiver for '%s'", platID)
return nil
}
codeCh <- code
return nil
}
func (ui *UI) OAuthHandlerTwitch(
ctx context.Context,
arg oauthhandler.OAuthHandlerArgument,
) error {
return ui.oauth2Handler(ctx, twitch.ID, arg)
}
func (ui *UI) OAuthHandlerYouTube(
ctx context.Context,
arg oauthhandler.OAuthHandlerArgument,
) error {
return ui.oauth2Handler(ctx, youtube.ID, arg)
}
func (ui *UI) InputTwitchUserInfo(
ctx context.Context,
cfg *streamcontrol.PlatformConfig[twitch.PlatformSpecificConfig, twitch.StreamProfile],
) (bool, error) {
return ui.InputTwitchUserInfoFn(ctx, cfg)
}
func (ui *UI) InputYouTubeUserInfo(
ctx context.Context,
cfg *streamcontrol.PlatformConfig[youtube.PlatformSpecificConfig, youtube.StreamProfile],
) (bool, error) {
return ui.InputYouTubeUserInfoFn(ctx, cfg)
}
func (ui *UI) InputOBSConnectInfo(
ctx context.Context,
cfg *streamcontrol.PlatformConfig[obs.PlatformSpecificConfig, obs.StreamProfile],
) (bool, error) {
return ui.InputOBSConnectInfoFn(ctx, cfg)
}