cremote/client/client.go

605 lines
13 KiB
Go

package client
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
)
// Client is the client for communicating with the daemon
type Client struct {
serverURL string
}
// Command represents a command sent from the client to the daemon
type Command struct {
Action string `json:"action"`
Params map[string]string `json:"params"`
}
// Response represents a response from the daemon to the client
type Response struct {
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}
// NewClient creates a new client
func NewClient(host string, port int) *Client {
return &Client{
serverURL: fmt.Sprintf("http://%s:%d", host, port),
}
}
// CheckStatus checks if the daemon is running
func (c *Client) CheckStatus() (bool, error) {
resp, err := http.Get(c.serverURL + "/status")
if err != nil {
return false, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
var response Response
err = json.NewDecoder(resp.Body).Decode(&response)
if err != nil {
return false, err
}
return response.Success, nil
}
// TabInfo contains information about a tab
type TabInfo struct {
ID string `json:"id"`
URL string `json:"url"`
IsCurrent bool `json:"is_current"`
HistoryIndex int `json:"history_index"` // Position in tab history (higher = more recent)
}
// ListTabs returns a list of all open tabs
func (c *Client) ListTabs() ([]TabInfo, error) {
resp, err := http.Get(c.serverURL + "/status")
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
var response Response
err = json.NewDecoder(resp.Body).Decode(&response)
if err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
if !response.Success {
return nil, fmt.Errorf("daemon returned error: %s", response.Error)
}
// Extract the data
data, ok := response.Data.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("unexpected response format")
}
// Get the tabs
tabsData, ok := data["tabs"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("unexpected tabs format")
}
// Get the current tab
currentTab, _ := data["current_tab"].(string)
// Get the tab history
tabHistoryData, ok := data["tab_history"].([]interface{})
if !ok {
tabHistoryData = []interface{}{}
}
// Create a map of tab history indices
tabHistoryIndices := make(map[string]int)
for i, idInterface := range tabHistoryData {
id, ok := idInterface.(string)
if ok {
tabHistoryIndices[id] = i
}
}
// Convert to TabInfo
tabs := make([]TabInfo, 0, len(tabsData))
for id, urlInterface := range tabsData {
url, ok := urlInterface.(string)
if !ok {
url = "<unknown>"
}
// Get the history index (default to -1 if not in history)
historyIndex, inHistory := tabHistoryIndices[id]
if !inHistory {
historyIndex = -1
}
tabs = append(tabs, TabInfo{
ID: id,
URL: url,
IsCurrent: id == currentTab,
HistoryIndex: historyIndex,
})
}
return tabs, nil
}
// SendCommand sends a command to the daemon
func (c *Client) SendCommand(action string, params map[string]string) (*Response, error) {
cmd := Command{
Action: action,
Params: params,
}
jsonData, err := json.Marshal(cmd)
if err != nil {
return nil, err
}
resp, err := http.Post(c.serverURL+"/command", "application/json", bytes.NewBuffer(jsonData))
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, body)
}
var response Response
err = json.NewDecoder(resp.Body).Decode(&response)
if err != nil {
return nil, err
}
return &response, nil
}
// OpenTab opens a new tab
// timeout is in seconds, 0 means no timeout
func (c *Client) OpenTab(timeout int) (string, error) {
params := map[string]string{}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("open-tab", params)
if err != nil {
return "", err
}
if !resp.Success {
return "", fmt.Errorf("failed to open tab: %s", resp.Error)
}
tabID, ok := resp.Data.(string)
if !ok {
return "", fmt.Errorf("unexpected response data type")
}
return tabID, nil
}
// LoadURL loads a URL in a tab
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) LoadURL(tabID, url string, timeout int) error {
params := map[string]string{
"url": url,
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("load-url", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to load URL: %s", resp.Error)
}
return nil
}
// FillFormField fills a form field with a value
// If tabID is empty, the current tab will be used
// selectionTimeout and actionTimeout are in seconds, 0 means no timeout
func (c *Client) FillFormField(tabID, selector, value string, selectionTimeout, actionTimeout int) error {
params := map[string]string{
"selector": selector,
"value": value,
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeouts if specified
if selectionTimeout > 0 {
params["selection-timeout"] = strconv.Itoa(selectionTimeout)
}
if actionTimeout > 0 {
params["action-timeout"] = strconv.Itoa(actionTimeout)
}
resp, err := c.SendCommand("fill-form", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to fill form field: %s", resp.Error)
}
return nil
}
// UploadFile uploads a file to a file input
// If tabID is empty, the current tab will be used
// selectionTimeout and actionTimeout are in seconds, 0 means no timeout
func (c *Client) UploadFile(tabID, selector, filePath string, selectionTimeout, actionTimeout int) error {
params := map[string]string{
"selector": selector,
"file": filePath,
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeouts if specified
if selectionTimeout > 0 {
params["selection-timeout"] = strconv.Itoa(selectionTimeout)
}
if actionTimeout > 0 {
params["action-timeout"] = strconv.Itoa(actionTimeout)
}
resp, err := c.SendCommand("upload-file", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to upload file: %s", resp.Error)
}
return nil
}
// SubmitForm submits a form
// If tabID is empty, the current tab will be used
// selectionTimeout and actionTimeout are in seconds, 0 means no timeout
func (c *Client) SubmitForm(tabID, selector string, selectionTimeout, actionTimeout int) error {
params := map[string]string{
"selector": selector,
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeouts if specified
if selectionTimeout > 0 {
params["selection-timeout"] = strconv.Itoa(selectionTimeout)
}
if actionTimeout > 0 {
params["action-timeout"] = strconv.Itoa(actionTimeout)
}
resp, err := c.SendCommand("submit-form", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to submit form: %s", resp.Error)
}
return nil
}
// GetPageSource gets the source code of a page
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) GetPageSource(tabID string, timeout int) (string, error) {
params := map[string]string{}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("get-source", params)
if err != nil {
return "", err
}
if !resp.Success {
return "", fmt.Errorf("failed to get page source: %s", resp.Error)
}
source, ok := resp.Data.(string)
if !ok {
return "", fmt.Errorf("unexpected response data type")
}
return source, nil
}
// GetElementHTML gets the HTML of an element
// If tabID is empty, the current tab will be used
// selectionTimeout is in seconds, 0 means no timeout
func (c *Client) GetElementHTML(tabID, selector string, selectionTimeout int) (string, error) {
params := map[string]string{
"selector": selector,
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if selectionTimeout > 0 {
params["selection-timeout"] = strconv.Itoa(selectionTimeout)
}
resp, err := c.SendCommand("get-element", params)
if err != nil {
return "", err
}
if !resp.Success {
return "", fmt.Errorf("failed to get element HTML: %s", resp.Error)
}
html, ok := resp.Data.(string)
if !ok {
return "", fmt.Errorf("unexpected response data type")
}
return html, nil
}
// CloseTab closes a tab
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) CloseTab(tabID string, timeout int) error {
params := map[string]string{}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("close-tab", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to close tab: %s", resp.Error)
}
return nil
}
// WaitNavigation waits for a navigation event
// If tabID is empty, the current tab will be used
func (c *Client) WaitNavigation(tabID string, timeout int) error {
params := map[string]string{
"timeout": fmt.Sprintf("%d", timeout),
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
resp, err := c.SendCommand("wait-navigation", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to wait for navigation: %s", resp.Error)
}
return nil
}
// EvalJS executes JavaScript code in a tab and returns the result
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) EvalJS(tabID, jsCode string, timeout int) (string, error) {
params := map[string]string{
"code": jsCode,
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("eval-js", params)
if err != nil {
return "", err
}
if !resp.Success {
return "", fmt.Errorf("failed to execute JavaScript: %s", resp.Error)
}
// Convert result to string if it exists
if resp.Data != nil {
if result, ok := resp.Data.(string); ok {
return result, nil
}
// If it's not a string, convert it to string representation
return fmt.Sprintf("%v", resp.Data), nil
}
return "", nil
}
// TakeScreenshot takes a screenshot of a tab and saves it to a file
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) TakeScreenshot(tabID, outputPath string, fullPage bool, timeout int) error {
params := map[string]string{
"output": outputPath,
"full-page": strconv.FormatBool(fullPage),
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("screenshot", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to take screenshot: %s", resp.Error)
}
return nil
}
// SwitchToIframe switches the context to an iframe for subsequent commands
// If tabID is empty, the current tab will be used
func (c *Client) SwitchToIframe(tabID, selector string) error {
params := map[string]string{
"selector": selector,
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
resp, err := c.SendCommand("switch-iframe", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to switch to iframe: %s", resp.Error)
}
return nil
}
// SwitchToMain switches the context back to the main page
// If tabID is empty, the current tab will be used
func (c *Client) SwitchToMain(tabID string) error {
params := map[string]string{}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
resp, err := c.SendCommand("switch-main", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to switch to main context: %s", resp.Error)
}
return nil
}
// ClickElement clicks on an element
// If tabID is empty, the current tab will be used
// selectionTimeout and actionTimeout are in seconds, 0 means no timeout
func (c *Client) ClickElement(tabID, selector string, selectionTimeout, actionTimeout int) error {
params := map[string]string{
"selector": selector,
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeouts if specified
if selectionTimeout > 0 {
params["selection-timeout"] = strconv.Itoa(selectionTimeout)
}
if actionTimeout > 0 {
params["action-timeout"] = strconv.Itoa(actionTimeout)
}
resp, err := c.SendCommand("click-element", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to click element: %s", resp.Error)
}
return nil
}