126 lines
2.9 KiB
Go
126 lines
2.9 KiB
Go
package webhooks
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
type Executor struct {
|
|
db *pgxpool.Pool
|
|
client *http.Client
|
|
logger *slog.Logger
|
|
}
|
|
|
|
func NewExecutor(db *pgxpool.Pool) *Executor {
|
|
return &Executor{
|
|
db: db,
|
|
client: &http.Client{
|
|
Timeout: 10 * time.Second,
|
|
},
|
|
logger: slog.Default().With("component", "webhooks"),
|
|
}
|
|
}
|
|
|
|
type MessageContext struct {
|
|
SenderName string `json:"sender_name"`
|
|
SenderEmail string `json:"sender_email"`
|
|
Subject string `json:"subject"`
|
|
BodyText string `json:"body_text"`
|
|
BodyHTML string `json:"body_html"`
|
|
Date string `json:"date"`
|
|
Recipients string `json:"recipients"`
|
|
HasAttachment bool `json:"has_attachment"`
|
|
MessageID string `json:"message_id"`
|
|
}
|
|
|
|
func (e *Executor) Execute(ctx context.Context, templateID string, msgCtx *MessageContext) error {
|
|
var (
|
|
url string
|
|
method string
|
|
headersJSON []byte
|
|
bodyTemplate string
|
|
)
|
|
|
|
err := e.db.QueryRow(ctx, `
|
|
SELECT url, method, headers, body_template
|
|
FROM webhook_templates
|
|
WHERE id = $1 AND is_active = true
|
|
`, templateID).Scan(&url, &method, &headersJSON, &bodyTemplate)
|
|
if err != nil {
|
|
return fmt.Errorf("query template: %w", err)
|
|
}
|
|
|
|
body := interpolate(bodyTemplate, msgCtx)
|
|
|
|
start := time.Now()
|
|
req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewBufferString(body))
|
|
if err != nil {
|
|
return fmt.Errorf("create request: %w", err)
|
|
}
|
|
|
|
var headers map[string]string
|
|
json.Unmarshal(headersJSON, &headers)
|
|
for k, v := range headers {
|
|
req.Header.Set(k, v)
|
|
}
|
|
if req.Header.Get("Content-Type") == "" {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
|
|
resp, err := e.client.Do(req)
|
|
duration := time.Since(start).Milliseconds()
|
|
|
|
var statusCode int
|
|
var responseBody string
|
|
var execError string
|
|
|
|
if err != nil {
|
|
execError = err.Error()
|
|
} else {
|
|
statusCode = resp.StatusCode
|
|
respBytes, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
|
resp.Body.Close()
|
|
responseBody = string(respBytes)
|
|
}
|
|
|
|
_, logErr := e.db.Exec(ctx, `
|
|
INSERT INTO webhook_logs (template_id, message_id, status_code, response_body, error, duration_ms)
|
|
VALUES ($1, $2, $3, $4, $5, $6)
|
|
`, templateID, msgCtx.MessageID, statusCode, responseBody, execError, duration)
|
|
if logErr != nil {
|
|
e.logger.Error("failed to log webhook", "error", logErr)
|
|
}
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("request failed: %w", err)
|
|
}
|
|
if statusCode >= 400 {
|
|
return fmt.Errorf("webhook returned %d", statusCode)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func interpolate(template string, ctx *MessageContext) string {
|
|
r := strings.NewReplacer(
|
|
"$sender.name", ctx.SenderName,
|
|
"$sender.email", ctx.SenderEmail,
|
|
"$subject", ctx.Subject,
|
|
"$body.textContent", ctx.BodyText,
|
|
"$body.htmlContent", ctx.BodyHTML,
|
|
"$date", ctx.Date,
|
|
"$recipients.to", ctx.Recipients,
|
|
"$message_id", ctx.MessageID,
|
|
)
|
|
return r.Replace(template)
|
|
}
|