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
This commit is contained in:
275
backend/internal/computer/connectors/webhook.go
Normal file
275
backend/internal/computer/connectors/webhook.go
Normal file
@@ -0,0 +1,275 @@
|
||||
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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user