Files
gooseek/backend/internal/computer/connectors/webhook.go
home 06fe57c765 feat: Go backend, enhanced search, new widgets, Docker deploy
Major changes:
- Add Go backend (backend/) with microservices architecture
- Enhanced master-agents-svc: reranker, content-classifier, stealth-crawler,
  proxy-manager, media-search, fastClassifier, language detection
- New web-svc widgets: KnowledgeCard, ProductCard, ProfileCard, VideoCard,
  UnifiedCard, CardGallery, InlineImageGallery, SourcesPanel, RelatedQuestions
- Improved discover-svc with discover-db integration
- Docker deployment improvements (Caddyfile, vendor.sh, BUILD.md)
- Library-svc: project_id schema migration
- Remove deprecated finance-svc and travel-svc
- Localization improvements across services

Made-with: Cursor
2026-02-27 04:15:32 +03:00

276 lines
6.7 KiB
Go

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,
})
}