diff --git a/cmd/root.go b/cmd/root.go index b42e1a2..b72c41a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -40,6 +40,7 @@ func init() { // initConfig reads and/or initializes the configuration file. func initConfig() { viper.SetDefault("averageSalary", 150000) + viper.SetDefault("currencySymbol", "$") // Find home directory. home, err := homedir.Dir() diff --git a/cmd/run.go b/cmd/run.go index 3a5b67f..696cbc5 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -32,19 +32,6 @@ var runCmd = &cobra.Command{ manual = true } - var scraper scrape.Scraper - if !manual { - // Checking optional force_jitsi flag first - switch { - case forceJitsi || strings.Contains(url, "meet.jit.si"): - scraper = scrape.GetParticipantsJitsi - case strings.Contains(url, "zoom"): - scraper = scrape.GetParticipantsZoom - default: - return fmt.Errorf("Provided url does not contain known domain") - } - } - // We declare data here because it's consumed by both the `tui` and // `scrape` packages. var data tui.Data @@ -56,10 +43,26 @@ var runCmd = &cobra.Command{ return err } + // Checking optional force_jitsi flag first + var meetingImpl scrape.MeetingImpl + switch { + case forceJitsi || strings.Contains(url, "meet.jit.si"): + meetingImpl = scrape.NewJitsi(url, pw) + case strings.Contains(url, "zoom"): + meetingImpl = scrape.NewZoom(url, pw) + default: + return fmt.Errorf("Provided url does not contain known domain") + } + log.Info("Initializing TUI.") - url, err := cmd.Flags().GetString("url") go func() { - err = scraper(url, 1, &data, pw) + meetingImpl.VisitMeetingUrl() + meetingImpl.FillBotName("clockwise-bot") + meetingImpl.JoinMeeting() + // FIXME: Deactivated until ffmpeg vcam gets implemented + meetingImpl.ActivateVirtualWebcam("") + + err = meetingImpl.GetParticipants(1, &data) if err != nil { log.Fatal(err) } diff --git a/cmd/set.go b/cmd/set.go index 0d655ce..b220303 100644 --- a/cmd/set.go +++ b/cmd/set.go @@ -12,39 +12,58 @@ import ( // setCmd represents the set command. var setCmd = &cobra.Command{ Use: "set", - Short: "Set the average annual salary of meeting participants", + Short: "Set the average annual salary of meeting participants and currency representation", Long: `Set the average annual salary of meeting participants. This does not need to be an exact number.`, PreRun: toggleDebug, RunE: func(cmd *cobra.Command, args []string) error { - averageSalary := viper.GetViper().GetInt("averageSalary") + // Fetch currently set values from config or default values + averageSalaryPrev := viper.GetViper().GetInt("averageSalary") + currencySymbolPrev := viper.GetViper().GetString("currencySymbol") - q := &survey.Question{ - Prompt: &survey.Input{ - Message: "Set average annual salary of meeting participants:", - Default: strconv.Itoa(averageSalary), - }, - Validate: func(val interface{}) error { - if _, err := strconv.Atoi(val.(string)); err != nil { - return err - } + q := []*survey.Question{ + { + Name: "averageSalary", + Prompt: &survey.Input{ + Message: "Set average annual salary of meeting participants:", + Default: strconv.Itoa(averageSalaryPrev), + }, + Validate: func(val interface{}) error { + if _, err := strconv.Atoi(val.(string)); err != nil { + return err + } - return nil + return nil + }, + }, + { + Name: "currencySymbol", + Prompt: &survey.Input{ + Message: "Set symbol or abbreviation of your local currency:", + Default: currencySymbolPrev, + }, }, } - err := survey.AskOne(q.Prompt, &averageSalary, survey.WithValidator(q.Validate)) + answers := struct { + AverageSalary int + CurrencySymbol string + }{} + + err := survey.Ask(q, &answers) if err != nil { return err } - viper.GetViper().Set("averageSalary", averageSalary) + viper.GetViper().Set("averageSalary", answers.AverageSalary) + viper.GetViper().Set("currencySymbol", answers.CurrencySymbol) if err := viper.WriteConfig(); err != nil { return err } log.Printf( - "The average annual salary of meeting participants has been updated to %v in the configuration file.", - averageSalary, + "The average annual salary of meeting participants has been updated to %s %v in the configuration file.", + answers.CurrencySymbol, + answers.AverageSalary, ) return nil diff --git a/internal/scrape/common.go b/internal/scrape/common.go index 609f7ae..a8553ab 100644 --- a/internal/scrape/common.go +++ b/internal/scrape/common.go @@ -3,12 +3,17 @@ package scrape import ( "fmt" - "github.com/syncfast/clockwise/internal/tui" "github.com/mxschmitt/playwright-go" + "github.com/syncfast/clockwise/internal/tui" ) -// Function prototype for per-platform participant count scraping -type Scraper func(url string, refreshInterval int, data *tui.Data, pw *playwright.Playwright) error +type MeetingImpl interface { + VisitMeetingUrl() error + FillBotName(botName string) error + JoinMeeting() error + ActivateVirtualWebcam(camName string) error + GetParticipants(refreshInterval int, data *tui.Data) error +} // initializePlaywright starts playwright in a standalone function to circumvent // some flaws in the upstream in terms of how it prints logs. diff --git a/internal/scrape/jitsi.go b/internal/scrape/jitsi.go index 55922f5..0f9a82e 100644 --- a/internal/scrape/jitsi.go +++ b/internal/scrape/jitsi.go @@ -10,66 +10,155 @@ import ( "github.com/syncfast/clockwise/internal/tui" ) -// GetParticipantsJitsi retrieves the total participant count from a specified -// Jitsi URL. It runs in a loop and updates the passed in `Data` struct every -// `refreshInterval` seconds. -func GetParticipantsJitsi(url string, refreshInterval int, data *tui.Data, pw *playwright.Playwright) error { - var timeout float64 = 5000 +type Jitsi struct { + url string + pw *playwright.Playwright + page playwright.Page + timeout float64 +} + +func NewJitsi(url string, pw *playwright.Playwright) *Jitsi { + return &Jitsi{ + url: url, + pw: pw, + page: nil, + timeout: 5000, + } +} - browser, err := pw.Chromium.Launch() +func (j *Jitsi) VisitMeetingUrl() error { + browser, err := j.pw.Chromium.Launch() if err != nil { return fmt.Errorf("could not launch browser: %w", err) } - page, err := browser.NewPage() + j.page, err = browser.NewPage() if err != nil { return fmt.Errorf("could not create page: %w", err) } - if _, err = page.Goto(url, playwright.PageGotoOptions{ + if _, err = j.page.Goto(j.url, playwright.PageGotoOptions{ WaitUntil: playwright.WaitUntilStateLoad, }); err != nil { return fmt.Errorf("could not goto: %w", err) } + return nil +} + +func (j *Jitsi) FillBotName(botName string) error { selector := "#Prejoin-input-field-id" - if err := page.Fill(selector, "clockwise-bot", playwright.FrameFillOptions{ - Timeout: &timeout, + if err := j.page.Fill(selector, botName, playwright.FrameFillOptions{ + Timeout: &j.timeout, }); err != nil { return err } + return nil +} + +func (j *Jitsi) JoinMeeting() error { // Wait for and click Join button - element, err := page.WaitForSelector("#lobby-screen > div.content > div.prejoin-input-area-container > div > div > div") + element, err := j.page.WaitForSelector("#lobby-screen > div.content > div.prejoin-input-area-container > div > div > div") if err != nil { return fmt.Errorf("failed to wait for join button: %w", err) } if err := element.Click(playwright.ElementHandleClickOptions{ - Timeout: &timeout, + Timeout: &j.timeout, }); err != nil { return err } + return nil +} + +func (j *Jitsi) ActivateVirtualWebcam(camName string) error { + fmt.Print("Locating camera activation button") + // Locate camera activation button + j.page.WaitForSelector("#new-toolbox > div > div > div > div.video-preview > div > div.toolbox-button") + res, err := j.page.QuerySelector("#new-toolbox > div > div > div > div.video-preview > div > div.toolbox-button") + if err != nil { + return err + } + + fmt.Print("Check if camera is activated") + // Check if camera is activated + button_state, err := res.GetAttribute("aria-pressed") + if err != nil { + return err + } + + fmt.Print("Camera activation button: ", button_state) + // If button is not pressed, press it + if button_state == "true" { + fmt.Print("Clicking on camera activation") + err = res.Click() + if err != nil { + return err + } + } + + fmt.Print("Check for video details (exposing available webcams)") + // Check for video details (exposing available webcams) + j.page.WaitForSelector("#video-settings-button") + res, err = j.page.QuerySelector("#video-settings-button") + if err != nil { + return err + } + + // Check if video settings (list of webcams) is expanded already + fmt.Print("Check if video settings (list of webcams) is expanded already") + button_state, err = res.GetAttribute("aria-expanded") + if err != nil { + return err + } + + fmt.Print("Camera settings button: ", button_state) + if button_state == "false" { + fmt.Print("Clicking on expand settings") + err = res.Click() + if err != nil { + return err + } + } + + fmt.Print("Grabbing Video Settings dialog") + j.page.WaitForSelector("#video-settings-dialog") + res, err = j.page.QuerySelector("#video-settings-dialog") + if err != nil { + return err + } + + fmt.Print("Printing inner html") + fmt.Print(res.InnerHTML()) + + return nil +} + +// GetParticipants retrieves the total participant count from a specified +// Jitsi URL. It runs in a loop and updates the passed in `Data` struct every +// `refreshInterval` seconds. +func (j *Jitsi) GetParticipants(refreshInterval int, data *tui.Data) error { // Wait for and click participants sidebar - element, err = page.WaitForSelector("#new-toolbox > div > div > div > div:nth-child(6)") + element, err := j.page.WaitForSelector("#new-toolbox > div > div > div > div:nth-child(6)") if err != nil { return fmt.Errorf("failed to wait for participant sidebar button: %w", err) } if err := element.Click(playwright.ElementHandleClickOptions{ - Timeout: &timeout, + Timeout: &j.timeout, }); err != nil { return err } - _, err = page.WaitForSelector("#layout_wrapper > div.participants_pane > div") + _, err = j.page.WaitForSelector("#layout_wrapper > div.participants_pane > div") if err != nil { return fmt.Errorf("failed to wait for participant sidebar: %w", err) } for { - res, err := page.QuerySelector("#layout_wrapper > div.participants_pane > div") + res, err := j.page.QuerySelector("#layout_wrapper > div.participants_pane > div") if err != nil { return err } diff --git a/internal/scrape/zoom.go b/internal/scrape/zoom.go index a9a7307..aa0da78 100644 --- a/internal/scrape/zoom.go +++ b/internal/scrape/zoom.go @@ -10,21 +10,32 @@ import ( "github.com/syncfast/clockwise/internal/tui" ) -// GetParticipantsZoom retrieves the total participant count from a specified -// zoom URL. It runs in a loop and updates the passed in `Data` struct every -// `refreshInterval` seconds. -func GetParticipantsZoom(url string, refreshInterval int, data *tui.Data, pw *playwright.Playwright) error { - if strings.Contains(url, "zoom.us/my/") { - return fmt.Errorf(`Error: clockwise is not compatible with Zoom Personal Meeting IDs at the moment. -Disabling your PMI is as as simple as clicking a checkbox. -Please visit https://support.zoom.us/hc/en-us/articles/203276937-Using-Personal-Meeting-ID-PMI- for more info.`) +type Zoom struct { + url string + pw *playwright.Playwright + page playwright.Page + timeout float64 +} + +func NewZoom(url string, pw *playwright.Playwright) *Zoom { + return &Zoom{ + url: url, + pw: pw, + page: nil, + timeout: 5000, } +} - var timeout float64 = 5000 +func (z *Zoom) VisitMeetingUrl() error { + if strings.Contains(z.url, "zoom.us/my/") { + return fmt.Errorf(`Error: clockwise is not compatible with Zoom Personal Meeting IDs at the moment. + Disabling your PMI is as as simple as clicking a checkbox. + Please visit https://support.zoom.us/hc/en-us/articles/203276937-Using-Personal-Meeting-ID-PMI- for more info.`) + } - url = mutateURL(url) + z.url = mutateURL(z.url) - browser, err := pw.Chromium.Launch() + browser, err := z.pw.Chromium.Launch() if err != nil { return fmt.Errorf("could not launch browser: %w", err) } @@ -34,37 +45,56 @@ Please visit https://support.zoom.us/hc/en-us/articles/203276937-Using-Personal- return fmt.Errorf("could not create page: %w", err) } - if _, err = page.Goto(url, playwright.PageGotoOptions{ + if _, err = page.Goto(z.url, playwright.PageGotoOptions{ WaitUntil: playwright.WaitUntilStateLoad, }); err != nil { return fmt.Errorf("could not goto: %w", err) } + return nil +} + +func (z *Zoom) FillBotName(botName string) error { selector := "text=Your Name" - if err := page.Fill(selector, "clockwise-bot", playwright.FrameFillOptions{ - Timeout: &timeout, + if err := z.page.Fill(selector, "clockwise-bot", playwright.FrameFillOptions{ + Timeout: &z.timeout, }); err != nil { return err } - element, err := page.WaitForSelector("button#joinBtn") + return nil +} + +func (z *Zoom) JoinMeeting() error { + element, err := z.page.WaitForSelector("button#joinBtn") if err != nil { return fmt.Errorf("failed to wait for join button: %w", err) } if err := element.Click(playwright.ElementHandleClickOptions{ - Timeout: &timeout, + Timeout: &z.timeout, }); err != nil { return err } - _, err = page.WaitForSelector(".footer-button__number-counter") + return nil +} + +func (z *Zoom) ActivateVirtualWebcam(camName string) error { + return nil +} + +// GetParticipants retrieves the total participant count from a specified +// zoom URL. It runs in a loop and updates the passed in `Data` struct every +// `refreshInterval` seconds. +func (z *Zoom) GetParticipants(refreshInterval int, data *tui.Data) error { + _, err := z.page.WaitForSelector(".footer-button__number-counter") if err != nil { - return fmt.Errorf("failed to wait for participant counter: %w", err) + return fmt.Errorf("failed to wait for join button: %w", err) } for { - res, err := page.QuerySelector(".footer-button__number-counter") + res, err := z.page.QuerySelector(".footer-button__number-counter") if err != nil { return err } diff --git a/internal/tui/tui.go b/internal/tui/tui.go index f90d149..c5ec292 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -47,6 +47,8 @@ func Start(manual bool, data *Data) { } averageSalary := viper.GetViper().GetInt("averageSalary") + currencySymbol := viper.GetViper().GetString("currencySymbol") + data.currencySymbol = currencySymbol // Start cost calculation goroutine. go func() { @@ -127,15 +129,16 @@ func Start(manual bool, data *Data) { tick(s, data, manual, quit) s.Fini() - log.Infof("Total cost: $%.2f", data.getCost()) + log.Infof("Total cost: %s%.2f", data.currencySymbol, data.getCost()) } // data stores variables passed around between the various goRoutines. type Data struct { - mtx sync.Mutex - count int - cost float32 - input string + mtx sync.Mutex + count int + cost float32 + input string + currencySymbol string } // Get count. @@ -221,7 +224,7 @@ func draw(s tcell.Screen, data *Data, manual bool) { style := tcell.StyleDefault.Foreground(tcell.ColorCornflowerBlue) emitStr(s, 0, 0, style, "Clockwise") - costString := fmt.Sprintf("Total cost: $%.2f", data.getCost()) + costString := fmt.Sprintf("Total cost: %s%.2f", data.currencySymbol, data.getCost()) emitStr(s, 0, 1, tcell.StyleDefault, costString) countString := fmt.Sprintf("Participant count: %s", strconv.Itoa((data.GetCount()))) @@ -252,7 +255,7 @@ func writeCostFile(data *Data) { outputFile := outputFolder + "clockwise.txt" for { - costString := fmt.Sprintf("Total cost: $%.2f\n", data.getCost()) + costString := fmt.Sprintf("Total cost: %s%.2f\n", data.currencySymbol, data.getCost()) if err := ioutil.WriteFile(outputFile, []byte(costString), 0600); err != nil { log.Fatal(err)