From 8355216f9badcdb4216b7f00d93df98fdcbdfbea Mon Sep 17 00:00:00 2001 From: tuxuser <462620+tuxuser@users.noreply.github.com> Date: Sun, 8 Aug 2021 06:41:10 +0200 Subject: [PATCH 1/6] Implement custom currency symbol/abbrevation (resolves issue #7) --- cmd/root.go | 1 + cmd/set.go | 22 +++++++++++++++++++--- internal/tui/tui.go | 10 +++++++--- 3 files changed, 27 insertions(+), 6 deletions(-) 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/set.go b/cmd/set.go index 0d655ce..2085912 100644 --- a/cmd/set.go +++ b/cmd/set.go @@ -17,8 +17,9 @@ var setCmd = &cobra.Command{ PreRun: toggleDebug, RunE: func(cmd *cobra.Command, args []string) error { averageSalary := viper.GetViper().GetInt("averageSalary") + currencySymbol := viper.GetViper().GetString("currencySymbol") - q := &survey.Question{ + qSalary := &survey.Question{ Prompt: &survey.Input{ Message: "Set average annual salary of meeting participants:", Default: strconv.Itoa(averageSalary), @@ -32,18 +33,33 @@ var setCmd = &cobra.Command{ }, } - err := survey.AskOne(q.Prompt, &averageSalary, survey.WithValidator(q.Validate)) + qCurrencySymbol := &survey.Question{ + Prompt: &survey.Input{ + Message: "Set symbol or abbrevation of your local currency:", + Default: "$", + }, + } + + err := survey.AskOne(qSalary.Prompt, &averageSalary, survey.WithValidator(qSalary.Validate)) + if err != nil { + return err + } + + // No validation required for currencySymbol + err = survey.AskOne(qCurrencySymbol.Prompt, ¤cySymbol) if err != nil { return err } viper.GetViper().Set("averageSalary", averageSalary) + viper.GetViper().Set("currencySymbol", 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.", + "The average annual salary of meeting participants has been updated to %s %v in the configuration file.", + currencySymbol, averageSalary, ) diff --git a/internal/tui/tui.go b/internal/tui/tui.go index f90d149..3d3726c 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -127,7 +127,7 @@ 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.GetCurrencySymbol(), data.getCost()) } // data stores variables passed around between the various goRoutines. @@ -138,6 +138,10 @@ type Data struct { input string } +func (data *Data) GetCurrencySymbol() string { + return viper.GetViper().GetString("currencySymbol") +} + // Get count. func (data *Data) GetCount() int { data.mtx.Lock() @@ -221,7 +225,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.GetCurrencySymbol(), data.getCost()) emitStr(s, 0, 1, tcell.StyleDefault, costString) countString := fmt.Sprintf("Participant count: %s", strconv.Itoa((data.GetCount()))) @@ -252,7 +256,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.GetCurrencySymbol(), data.getCost()) if err := ioutil.WriteFile(outputFile, []byte(costString), 0600); err != nil { log.Fatal(err) From 21646ecd04c15635b8aebb633c6490f1093cdec0 Mon Sep 17 00:00:00 2001 From: tuxuser <462620+tuxuser@users.noreply.github.com> Date: Sun, 8 Aug 2021 06:44:50 +0200 Subject: [PATCH 2/6] Fix: Spelling --- cmd/set.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/set.go b/cmd/set.go index 2085912..0a60ab1 100644 --- a/cmd/set.go +++ b/cmd/set.go @@ -12,7 +12,7 @@ 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 { @@ -35,7 +35,7 @@ var setCmd = &cobra.Command{ qCurrencySymbol := &survey.Question{ Prompt: &survey.Input{ - Message: "Set symbol or abbrevation of your local currency:", + Message: "Set symbol or abbreviation of your local currency:", Default: "$", }, } From 651dd40fa3e8c489d1910f639512a666413eb0d1 Mon Sep 17 00:00:00 2001 From: tuxuser <462620+tuxuser@users.noreply.github.com> Date: Sun, 8 Aug 2021 18:24:05 +0200 Subject: [PATCH 3/6] Set subcommand: Bundle survey questions into single structure --- cmd/set.go | 59 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/cmd/set.go b/cmd/set.go index 0a60ab1..b220303 100644 --- a/cmd/set.go +++ b/cmd/set.go @@ -16,51 +16,54 @@ var setCmd = &cobra.Command{ 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") - currencySymbol := viper.GetViper().GetString("currencySymbol") + // Fetch currently set values from config or default values + averageSalaryPrev := viper.GetViper().GetInt("averageSalary") + currencySymbolPrev := viper.GetViper().GetString("currencySymbol") - qSalary := &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 + }, }, - } - - qCurrencySymbol := &survey.Question{ - Prompt: &survey.Input{ - Message: "Set symbol or abbreviation of your local currency:", - Default: "$", + { + Name: "currencySymbol", + Prompt: &survey.Input{ + Message: "Set symbol or abbreviation of your local currency:", + Default: currencySymbolPrev, + }, }, } - err := survey.AskOne(qSalary.Prompt, &averageSalary, survey.WithValidator(qSalary.Validate)) - if err != nil { - return err - } + answers := struct { + AverageSalary int + CurrencySymbol string + }{} - // No validation required for currencySymbol - err = survey.AskOne(qCurrencySymbol.Prompt, ¤cySymbol) + err := survey.Ask(q, &answers) if err != nil { return err } - viper.GetViper().Set("averageSalary", averageSalary) - viper.GetViper().Set("currencySymbol", currencySymbol) + 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 %s %v in the configuration file.", - currencySymbol, - averageSalary, + answers.CurrencySymbol, + answers.AverageSalary, ) return nil From 42861ee9f4c888605a4e3c1e85d1c0f61eb93d93 Mon Sep 17 00:00:00 2001 From: tuxuser <462620+tuxuser@users.noreply.github.com> Date: Sun, 8 Aug 2021 18:31:36 +0200 Subject: [PATCH 4/6] TUI: Access CurrencySymbol as const class member, instead of calling Viper.GetString('currencySymbol') over and over --- internal/tui/tui.go | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 3d3726c..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,19 +129,16 @@ func Start(manual bool, data *Data) { tick(s, data, manual, quit) s.Fini() - log.Infof("Total cost: %s%.2f", data.GetCurrencySymbol(), 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 -} - -func (data *Data) GetCurrencySymbol() string { - return viper.GetViper().GetString("currencySymbol") + mtx sync.Mutex + count int + cost float32 + input string + currencySymbol string } // Get count. @@ -225,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: %s%.2f", data.GetCurrencySymbol(), 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()))) @@ -256,7 +255,7 @@ func writeCostFile(data *Data) { outputFile := outputFolder + "clockwise.txt" for { - costString := fmt.Sprintf("Total cost: %s%.2f\n", data.GetCurrencySymbol(), 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) From 5148c3d22cbd2767a386d91e50e8fbec94cab056 Mon Sep 17 00:00:00 2001 From: tuxuser <462620+tuxuser@users.noreply.github.com> Date: Wed, 4 Aug 2021 18:16:44 +0200 Subject: [PATCH 5/6] Scrape: Modularize scraping into interface and single steps --- cmd/run.go | 33 ++++++++++--------- internal/scrape/common.go | 11 +++++-- internal/scrape/jitsi.go | 62 ++++++++++++++++++++++++++--------- internal/scrape/zoom.go | 68 ++++++++++++++++++++++++++++----------- 4 files changed, 121 insertions(+), 53 deletions(-) diff --git a/cmd/run.go b/cmd/run.go index 3a5b67f..acd5a03 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/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..a214362 100644 --- a/internal/scrape/jitsi.go +++ b/internal/scrape/jitsi.go @@ -10,66 +10,96 @@ 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 { + 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 } From ca03df9399586e0756e7187c1b4e9de0e970f30b Mon Sep 17 00:00:00 2001 From: tuxuser <462620+tuxuser@users.noreply.github.com> Date: Sat, 7 Aug 2021 15:33:12 +0200 Subject: [PATCH 6/6] WIP: Activate webcam --- cmd/run.go | 2 +- internal/scrape/jitsi.go | 59 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/cmd/run.go b/cmd/run.go index acd5a03..696cbc5 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -60,7 +60,7 @@ var runCmd = &cobra.Command{ meetingImpl.FillBotName("clockwise-bot") meetingImpl.JoinMeeting() // FIXME: Deactivated until ffmpeg vcam gets implemented - // meetingImpl.ActivateVirtualWebcam("") + meetingImpl.ActivateVirtualWebcam("") err = meetingImpl.GetParticipants(1, &data) if err != nil { diff --git a/internal/scrape/jitsi.go b/internal/scrape/jitsi.go index a214362..0f9a82e 100644 --- a/internal/scrape/jitsi.go +++ b/internal/scrape/jitsi.go @@ -74,6 +74,65 @@ func (j *Jitsi) JoinMeeting() error { } 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 }