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
276 lines
6.7 KiB
Go
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,
|
|
})
|
|
}
|