mirror of
https://github.com/xaionaro-go/streamctl.git
synced 2025-10-12 19:00:36 +08:00
250 lines
7.0 KiB
Go
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)
|
|
}
|