Initial commit, pt. 10

This commit is contained in:
Dmitrii Okunev
2024-06-04 00:37:23 +01:00
parent 56f78af025
commit ada2d30192
8 changed files with 187 additions and 80 deletions

View File

@@ -6,47 +6,36 @@ import (
"log"
"net"
"net/http"
"net/url"
"os/exec"
"runtime"
"github.com/facebookincubator/go-belt/tool/logger"
)
type OAuth2Handler struct {
authURL string
exchangeFn func(code string) error
receiverAddr string
}
func NewOAuth2Handler(
authURL string,
exchangeFn func(code string) error,
receiverAddr string,
) *OAuth2Handler {
return &OAuth2Handler{
authURL: authURL,
exchangeFn: exchangeFn,
receiverAddr: receiverAddr,
}
type OAuthHandlerArgument struct {
AuthURL string
RedirectURL string
ExchangeFn func(code string) error
}
// it is guaranteed exchangeFn was called if error is nil.
func (h *OAuth2Handler) Handle(ctx context.Context) error {
if h.receiverAddr != "" {
err := h.handleViaBrowser()
func OAuth2Handler(ctx context.Context, arg OAuthHandlerArgument) error {
if arg.RedirectURL != "" {
err := OAuth2HandlerViaBrowser(ctx, arg)
if err == nil {
return nil
}
logger.Errorf(ctx, "unable to authenticate automatically: %v", err)
}
return h.handleViaCLI()
return OAuth2HandlerViaCLI(ctx, arg)
}
func (h *OAuth2Handler) handleViaCLI() error {
func OAuth2HandlerViaCLI(ctx context.Context, arg OAuthHandlerArgument) error {
fmt.Printf(
"It is required to get an oauth2 token. "+
"Please open the link below in the browser:\n\n\t%s\n\n",
h.authURL,
arg.AuthURL,
)
fmt.Printf("Enter the code: ")
@@ -54,30 +43,35 @@ func (h *OAuth2Handler) handleViaCLI() error {
if _, err := fmt.Scan(&code); err != nil {
log.Fatalf("Unable to read authorization code %v", err)
}
return h.exchangeFn(code)
return arg.ExchangeFn(code)
}
func (h *OAuth2Handler) handleViaBrowser() error {
codeCh, err := h.newCodeReceiver()
func OAuth2HandlerViaBrowser(ctx context.Context, arg OAuthHandlerArgument) error {
codeCh, err := NewCodeReceiver(arg.RedirectURL)
if err != nil {
return err
}
err = launchBrowser(h.authURL)
err = LaunchBrowser(arg.AuthURL)
if err != nil {
return err
}
fmt.Printf("Your browser has been launched (URL: %s).\nPlease approve the permissions.\n", h.authURL)
fmt.Printf("Your browser has been launched (URL: %s).\nPlease approve the permissions.\n", arg.AuthURL)
// Wait for the web server to get the code.
code := <-codeCh
return h.exchangeFn(code)
return arg.ExchangeFn(code)
}
func (h *OAuth2Handler) newCodeReceiver() (codeCh chan string, err error) {
func NewCodeReceiver(redirectURL string) (codeCh chan string, err error) {
urlParsed, err := url.Parse(redirectURL)
if err != nil {
return nil, fmt.Errorf("unable to parse URL '%s': %w", redirectURL, err)
}
// this function was mostly borrowed from https://developers.google.com/youtube/v3/code_samples/go#authorize_a_request
listener, err := net.Listen("tcp", h.receiverAddr)
listener, err := net.Listen("tcp", urlParsed.Host)
if err != nil {
return nil, err
}
@@ -94,7 +88,7 @@ func (h *OAuth2Handler) newCodeReceiver() (codeCh chan string, err error) {
return codeCh, nil
}
func launchBrowser(url string) error {
func LaunchBrowser(url string) error {
switch runtime.GOOS {
case "darwin":
return exec.Command("open", url).Start()

View File

@@ -200,6 +200,8 @@ func GetPlatformSpecificConfig[T any](
switch platCfgCfg := platCfgCfg.(type) {
case T:
return platCfgCfg
case *T:
return *platCfgCfg
case RawMessage:
var v T
err := yaml.Unmarshal(platCfgCfg, &v)

View File

@@ -1,20 +1,26 @@
package twitch
import (
"context"
"github.com/xaionaro-go/streamctl/pkg/oauthhandler"
streamctl "github.com/xaionaro-go/streamctl/pkg/streamcontrol"
)
const ID = streamctl.PlatformName("twitch")
type OAuthHandler func(context.Context, oauthhandler.OAuthHandlerArgument) error
type PlatformSpecificConfig struct {
Channel string
ClientID string
ClientSecret string
ClientCode string
AuthType string
AppAccessToken string
UserAccessToken string
RefreshToken string
Channel string
ClientID string
ClientSecret string
ClientCode string
AuthType string
AppAccessToken string
UserAccessToken string
RefreshToken string
CustomOAuthHandler OAuthHandler `yaml:"-"`
}
type Config = streamctl.PlatformConfig[PlatformSpecificConfig, StreamProfile]

View File

@@ -208,11 +208,12 @@ func getClient(
cfg Config,
safeCfgFn func(Config) error,
) (*helix.Client, error) {
client, err := helix.NewClient(&helix.Options{
options := &helix.Options{
ClientID: cfg.Config.ClientID,
ClientSecret: cfg.Config.ClientSecret,
RedirectURI: "http://localhost/",
})
RedirectURI: "http://localhost:8091/",
}
client, err := helix.NewClient(options)
if err != nil {
return nil, fmt.Errorf("unable to create a helix client object: %w", err)
}
@@ -236,13 +237,23 @@ func getClient(
Scopes: []string{"channel:manage:broadcast"},
})
oauthHandler := oauthhandler.NewOAuth2Handler(authURL, func(code string) error {
cfg.Config.ClientCode = code
err = safeCfgFn(cfg)
errmon.ObserveErrorCtx(ctx, err)
return nil
}, "")
err := oauthHandler.Handle(ctx)
arg := oauthhandler.OAuthHandlerArgument{
AuthURL: authURL,
RedirectURL: options.RedirectURI,
ExchangeFn: func(code string) error {
cfg.Config.ClientCode = code
err = safeCfgFn(cfg)
errmon.ObserveErrorCtx(ctx, err)
return nil
},
}
oauthHandler := cfg.Config.CustomOAuthHandler
if oauthHandler == nil {
oauthHandler = oauthhandler.OAuth2HandlerViaCLI
}
err := oauthHandler(ctx, arg)
if err != nil {
return nil, fmt.Errorf("unable to get or exchange the oauth code to a token: %w", err)
}

View File

@@ -1,16 +1,22 @@
package youtube
import (
"context"
"github.com/xaionaro-go/streamctl/pkg/oauthhandler"
streamctl "github.com/xaionaro-go/streamctl/pkg/streamcontrol"
"golang.org/x/oauth2"
)
const ID = streamctl.PlatformName("youtube")
type OAuthHandler func(context.Context, oauthhandler.OAuthHandlerArgument) error
type PlatformSpecificConfig struct {
ClientID string
ClientSecret string
Token *oauth2.Token
ClientID string
ClientSecret string
Token *oauth2.Token
CustomOAuthHandler OAuthHandler `yaml:"-"`
}
type Config = streamctl.PlatformConfig[PlatformSpecificConfig, StreamProfile]

View File

@@ -122,18 +122,26 @@ func getAuthCfg(cfg Config) *oauth2.Config {
func getToken(ctx context.Context, cfg Config) (*oauth2.Token, error) {
googleAuthCfg := getAuthCfg(cfg)
authURL := googleAuthCfg.AuthCodeURL("state-token", oauth2.AccessTypeOffline)
var tok *oauth2.Token
oauthHandler := oauthhandler.NewOAuth2Handler(authURL, func(code string) error {
_tok, err := googleAuthCfg.Exchange(ctx, code)
if err != nil {
return fmt.Errorf("unable to get a token: %w", err)
}
tok = _tok
return nil
}, "")
err := oauthHandler.Handle(ctx)
oauthHandlerArg := oauthhandler.OAuthHandlerArgument{
AuthURL: googleAuthCfg.AuthCodeURL("state-token", oauth2.AccessTypeOffline),
RedirectURL: googleAuthCfg.RedirectURL,
ExchangeFn: func(code string) error {
_tok, err := googleAuthCfg.Exchange(ctx, code)
if err != nil {
return fmt.Errorf("unable to get a token: %w", err)
}
tok = _tok
return nil
},
}
oauthHandler := cfg.Config.CustomOAuthHandler
if oauthHandler == nil {
oauthHandler = oauthhandler.OAuth2HandlerViaCLI
}
err := oauthHandler(ctx, oauthHandlerArg)
if err != nil {
return nil, err
}

View File

@@ -5,7 +5,9 @@ import (
_ "embed"
"fmt"
"os"
"os/exec"
"path"
"runtime"
"runtime/debug"
"sort"
"strings"
@@ -20,6 +22,7 @@ import (
"github.com/facebookincubator/go-belt/tool/experimental/errmon"
"github.com/facebookincubator/go-belt/tool/logger"
"github.com/go-ng/xmath"
"github.com/xaionaro-go/streamctl/pkg/oauthhandler"
"github.com/xaionaro-go/streamctl/pkg/streamcontrol"
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/twitch"
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/youtube"
@@ -35,6 +38,7 @@ type Panel struct {
dataLock sync.Mutex
data panelData
app fyne.App
config config
startStopMutex sync.Mutex
updateTimerHandler *updateTimerHandler
@@ -77,23 +81,24 @@ func (p *Panel) Loop(ctx context.Context) error {
return fmt.Errorf("unable to load the data '%s': %w", p.dataPath, err)
}
p.app = fyneapp.New()
if err := p.initStreamControllers(); err != nil {
return fmt.Errorf("unable to initialize stream controllers: %w", err)
}
a := fyneapp.New()
p.initTwitchData(a)
p.initTwitchData()
p.normalizeTwitchData()
p.initYoutubeData(a)
p.initYoutubeData()
p.normalizeYoutubeData()
p.initMainWindow(ctx, a)
p.initMainWindow(ctx)
p.mainWindow.ShowAndRun()
return nil
}
func (p *Panel) initTwitchData(a fyne.App) {
func (p *Panel) initTwitchData() {
logger.FromCtx(p.defaultContext).Debugf("initializing Twitch data")
defer logger.FromCtx(p.defaultContext).Debugf("endof initializing Twitch data")
@@ -110,7 +115,7 @@ func (p *Panel) initTwitchData(a fyne.App) {
allCategories, err := twitch.GetAllCategories(p.defaultContext)
if err != nil {
p.displayError(a, err)
p.displayError(err)
return
}
@@ -133,7 +138,7 @@ func (p *Panel) normalizeTwitchData() {
})
}
func (p *Panel) initYoutubeData(a fyne.App) {
func (p *Panel) initYoutubeData() {
logger.FromCtx(p.defaultContext).Debugf("initializing Youtube data")
defer logger.FromCtx(p.defaultContext).Debugf("endof initializing Youtube data")
@@ -150,7 +155,7 @@ func (p *Panel) initYoutubeData(a fyne.App) {
broadcasts, err := youtube.ListBroadcasts(p.defaultContext)
if err != nil {
p.displayError(a, err)
p.displayError(err)
return
}
@@ -223,6 +228,77 @@ func (p *Panel) savePlatformConfig(
return p.saveData()
}
func (p *Panel) oauthHandlerTwitch(ctx context.Context, arg oauthhandler.OAuthHandlerArgument) error {
logger.Infof(ctx, "oauthHandlerTwitch: %#+v", arg)
defer logger.Infof(ctx, "/oauthHandlerTwitch")
return p.oauthHandler(ctx, arg)
}
func (p *Panel) oauthHandlerYouTube(ctx context.Context, arg oauthhandler.OAuthHandlerArgument) error {
logger.Infof(ctx, "oauthHandlerYouTube: %#+v", arg)
defer logger.Infof(ctx, "/oauthHandlerYouTube")
return p.oauthHandler(ctx, arg)
}
func (p *Panel) oauthHandler(ctx context.Context, arg oauthhandler.OAuthHandlerArgument) error {
codeCh, err := oauthhandler.NewCodeReceiver(arg.RedirectURL)
if err != nil {
return err
}
if err := p.openBrowser(arg.AuthURL); err != nil {
return fmt.Errorf("unable to open browser with URL '%s': %w", arg.AuthURL, err)
}
logger.Infof(ctx, "Your browser has been launched (URL: %s).\nPlease approve the permissions.\n", arg.AuthURL)
// Wait for the web server to get the code.
code := <-codeCh
logger.Debugf(ctx, "received the auth code")
return arg.ExchangeFn(code)
}
func (p *Panel) openBrowser(authURL string) error {
var browserCmd string
switch runtime.GOOS {
case "darwin":
browserCmd = "open"
case "linux":
browserCmd = "xdg-open"
default:
return oauthhandler.LaunchBrowser(authURL)
}
waitCh := make(chan struct{})
w := p.app.NewWindow("Browser selection window")
browserField := widget.NewEntry()
browserField.PlaceHolder = "command to execute the browser"
browserField.OnSubmitted = func(s string) {
close(waitCh)
}
okButton := widget.NewButton("OK", func() {
close(waitCh)
})
w.SetContent(container.NewBorder(
browserField,
okButton,
nil,
nil,
nil,
))
go w.ShowAndRun()
<-waitCh
w.Close()
if browserField.Text != "" {
browserCmd = browserField.Text
}
return exec.Command(browserCmd, authURL).Start()
}
func (p *Panel) initStreamControllers() error {
for platName, cfg := range p.data.Backends {
var err error
@@ -230,11 +306,11 @@ func (p *Panel) initStreamControllers() error {
case strings.ToLower(string(twitch.ID)):
p.streamControllers.Twitch, err = newTwitch(p.defaultContext, cfg, func(cfg *streamcontrol.AbstractPlatformConfig) error {
return p.savePlatformConfig(twitch.ID, cfg)
})
}, p.oauthHandlerTwitch)
case strings.ToLower(string(youtube.ID)):
p.streamControllers.YouTube, err = newYouTube(p.defaultContext, cfg, func(cfg *streamcontrol.AbstractPlatformConfig) error {
return p.savePlatformConfig(youtube.ID, cfg)
})
}, p.oauthHandlerYouTube)
}
if err != nil {
return fmt.Errorf("unable to initialize '%s': %w", platName, err)
@@ -432,8 +508,8 @@ func (p *Panel) setFilter(filter string) {
p.refilterProfiles()
}
func (p *Panel) initMainWindow(ctx context.Context, a fyne.App) {
w := a.NewWindow("TimeTracker")
func (p *Panel) initMainWindow(ctx context.Context) {
w := p.app.NewWindow("StreamPanel")
p.startStopButton = widget.NewButtonWithIcon("", theme.MediaPlayIcon(), p.onStartStopButton)
p.startStopButton.Importance = widget.SuccessImportance
@@ -449,7 +525,7 @@ func (p *Panel) initMainWindow(ctx context.Context, a fyne.App) {
topPanel := container.NewVBox(
container.NewHBox(
widget.NewButtonWithIcon("Profile", theme.ContentAddIcon(), func() {
p.newProfileWindow(ctx, a)
p.newProfileWindow(ctx)
}),
),
profileFilter,
@@ -512,13 +588,13 @@ func cleanYoutubeRecordingName(in string) string {
return strings.ToLower(strings.Trim(in, " "))
}
func (p *Panel) newProfileWindow(_ context.Context, a fyne.App) fyne.Window {
func (p *Panel) newProfileWindow(_ context.Context) fyne.Window {
var (
twitchProfile *twitch.StreamProfile
youtubeProfile *youtube.StreamProfile
)
w := a.NewWindow("Create a profile")
w := p.app.NewWindow("Create a profile")
w.Resize(fyne.NewSize(400, 300))
activityTitle := widget.NewEntry()
activityTitle.SetPlaceHolder("title")
@@ -682,7 +758,7 @@ func (p *Panel) newProfileWindow(_ context.Context, a fyne.App) fyne.Window {
}
err := fmt.Errorf("creating a profile is not implemented")
if err != nil {
p.displayError(a, err)
p.displayError(err)
return
}
w.Close()
@@ -704,8 +780,8 @@ func (p *Panel) newProfileWindow(_ context.Context, a fyne.App) fyne.Window {
return w
}
func (p *Panel) displayError(a fyne.App, err error) {
w := a.NewWindow("Got an error: " + err.Error())
func (p *Panel) displayError(err error) {
w := p.app.NewWindow("Got an error: " + err.Error())
errorMessage := fmt.Sprintf("Error: %v\n\nstack trace:\n%s", err, debug.Stack())
w.Resize(fyne.NewSize(400, 300))
w.SetContent(widget.NewLabelWithStyle(errorMessage, fyne.TextAlignLeading, fyne.TextStyle{Bold: true}))

View File

@@ -14,6 +14,7 @@ func newTwitch(
ctx context.Context,
cfg *streamcontrol.AbstractPlatformConfig,
saveCfgFunc func(*streamcontrol.AbstractPlatformConfig) error,
customOAuthHandler twitch.OAuthHandler,
) (
*twitch.Twitch,
error,
@@ -26,6 +27,7 @@ func newTwitch(
}
logger.Debugf(ctx, "twitch config: %#+v", platCfg)
platCfg.Config.CustomOAuthHandler = customOAuthHandler
return twitch.New(ctx, *platCfg,
func(c twitch.Config) error {
return saveCfgFunc(&streamcontrol.AbstractPlatformConfig{
@@ -40,6 +42,7 @@ func newYouTube(
ctx context.Context,
cfg *streamcontrol.AbstractPlatformConfig,
saveCfgFunc func(*streamcontrol.AbstractPlatformConfig) error,
customOAuthHandler youtube.OAuthHandler,
) (
*youtube.YouTube,
error,
@@ -52,6 +55,7 @@ func newYouTube(
}
logger.Debugf(ctx, "youtube config: %#+v", platCfg)
platCfg.Config.CustomOAuthHandler = customOAuthHandler
return youtube.New(ctx, *platCfg,
func(c youtube.Config) error {
return saveCfgFunc(&streamcontrol.AbstractPlatformConfig{