Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
33 changes: 18 additions & 15 deletions cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
Expand Down
51 changes: 35 additions & 16 deletions cmd/set.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 8 additions & 3 deletions internal/scrape/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
121 changes: 105 additions & 16 deletions internal/scrape/jitsi.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Loading