package connectors import ( "bytes" "context" "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "strings" "time" ) type WebhookConfig struct { Timeout time.Duration MaxRetries int RetryDelay time.Duration DefaultSecret string } type WebhookConnector struct { cfg WebhookConfig client *http.Client } func NewWebhookConnector(cfg WebhookConfig) *WebhookConnector { timeout := cfg.Timeout if timeout == 0 { timeout = 30 * time.Second } if cfg.MaxRetries == 0 { cfg.MaxRetries = 3 } if cfg.RetryDelay == 0 { cfg.RetryDelay = time.Second } return &WebhookConnector{ cfg: cfg, client: &http.Client{ Timeout: timeout, }, } } func (w *WebhookConnector) ID() string { return "webhook" } func (w *WebhookConnector) Name() string { return "Webhook" } func (w *WebhookConnector) Description() string { return "Send HTTP webhooks to external services" } func (w *WebhookConnector) GetActions() []Action { return []Action{ { Name: "post", Description: "Send POST request", Schema: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "url": map[string]interface{}{"type": "string", "description": "Webhook URL"}, "body": map[string]interface{}{"type": "object", "description": "Request body (JSON)"}, "headers": map[string]interface{}{"type": "object", "description": "Custom headers"}, "secret": map[string]interface{}{"type": "string", "description": "HMAC secret for signing"}, }, }, Required: []string{"url"}, }, { Name: "get", Description: "Send GET request", Schema: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "url": map[string]interface{}{"type": "string", "description": "Request URL"}, "params": map[string]interface{}{"type": "object", "description": "Query parameters"}, "headers": map[string]interface{}{"type": "object", "description": "Custom headers"}, }, }, Required: []string{"url"}, }, { Name: "put", Description: "Send PUT request", Schema: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "url": map[string]interface{}{"type": "string", "description": "Request URL"}, "body": map[string]interface{}{"type": "object", "description": "Request body (JSON)"}, "headers": map[string]interface{}{"type": "object", "description": "Custom headers"}, }, }, Required: []string{"url"}, }, } } func (w *WebhookConnector) Validate(params map[string]interface{}) error { urlStr, ok := params["url"].(string) if !ok { return errors.New("'url' is required") } parsed, err := url.Parse(urlStr) if err != nil { return fmt.Errorf("invalid URL: %w", err) } if parsed.Scheme != "http" && parsed.Scheme != "https" { return errors.New("URL must use http or https scheme") } return nil } func (w *WebhookConnector) Execute(ctx context.Context, action string, params map[string]interface{}) (interface{}, error) { switch action { case "post": return w.doRequest(ctx, "POST", params) case "get": return w.doRequest(ctx, "GET", params) case "put": return w.doRequest(ctx, "PUT", params) case "delete": return w.doRequest(ctx, "DELETE", params) case "patch": return w.doRequest(ctx, "PATCH", params) default: return nil, errors.New("unknown action: " + action) } } func (w *WebhookConnector) doRequest(ctx context.Context, method string, params map[string]interface{}) (interface{}, error) { urlStr := params["url"].(string) if method == "GET" { if queryParams, ok := params["params"].(map[string]interface{}); ok { parsedURL, _ := url.Parse(urlStr) q := parsedURL.Query() for k, v := range queryParams { q.Set(k, fmt.Sprintf("%v", v)) } parsedURL.RawQuery = q.Encode() urlStr = parsedURL.String() } } var bodyReader io.Reader var bodyBytes []byte if body, ok := params["body"]; ok && method != "GET" { var err error bodyBytes, err = json.Marshal(body) if err != nil { return nil, fmt.Errorf("failed to marshal body: %w", err) } bodyReader = bytes.NewReader(bodyBytes) } var lastErr error for attempt := 0; attempt <= w.cfg.MaxRetries; attempt++ { if attempt > 0 { time.Sleep(w.cfg.RetryDelay * time.Duration(attempt)) if bodyBytes != nil { bodyReader = bytes.NewReader(bodyBytes) } } req, err := http.NewRequestWithContext(ctx, method, urlStr, bodyReader) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") req.Header.Set("User-Agent", "GooSeek-Computer/1.0") if headers, ok := params["headers"].(map[string]interface{}); ok { for k, v := range headers { req.Header.Set(k, fmt.Sprintf("%v", v)) } } if bodyBytes != nil { secret := w.cfg.DefaultSecret if s, ok := params["secret"].(string); ok { secret = s } if secret != "" { signature := w.signPayload(bodyBytes, secret) req.Header.Set("X-Signature-256", "sha256="+signature) } } resp, err := w.client.Do(req) if err != nil { lastErr = err continue } respBody, err := io.ReadAll(resp.Body) resp.Body.Close() if err != nil { lastErr = err continue } result := map[string]interface{}{ "status_code": resp.StatusCode, "headers": w.headersToMap(resp.Header), } var jsonBody interface{} if err := json.Unmarshal(respBody, &jsonBody); err == nil { result["body"] = jsonBody } else { result["body"] = string(respBody) } if resp.StatusCode >= 200 && resp.StatusCode < 300 { result["success"] = true return result, nil } if resp.StatusCode >= 500 { lastErr = fmt.Errorf("server error: %d", resp.StatusCode) continue } result["success"] = false return result, nil } return map[string]interface{}{ "success": false, "error": lastErr.Error(), }, lastErr } func (w *WebhookConnector) signPayload(payload []byte, secret string) string { mac := hmac.New(sha256.New, []byte(secret)) mac.Write(payload) return hex.EncodeToString(mac.Sum(nil)) } func (w *WebhookConnector) headersToMap(headers http.Header) map[string]string { result := make(map[string]string) for k, v := range headers { result[k] = strings.Join(v, ", ") } return result } func (w *WebhookConnector) PostJSON(ctx context.Context, webhookURL string, data interface{}) (interface{}, error) { return w.Execute(ctx, "post", map[string]interface{}{ "url": webhookURL, "body": data, }) } func (w *WebhookConnector) GetJSON(ctx context.Context, webhookURL string, params map[string]interface{}) (interface{}, error) { return w.Execute(ctx, "get", map[string]interface{}{ "url": webhookURL, "params": params, }) }