package webhook import ( "bytes" "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "time" "wedding-server/internal/config" "wedding-server/internal/store" ) type Client struct { cfg config.WebhookConfig httpClient *http.Client } func New(cfg config.WebhookConfig) *Client { return &Client{ cfg: cfg, httpClient: &http.Client{Timeout: time.Duration(cfg.TimeoutSeconds) * time.Second}, } } func (c *Client) post(url, event string, data any) error { if url == "" { return nil } body, err := json.Marshal(map[string]any{ "event": event, "sentAt": time.Now().UTC(), "data": data, }) if err != nil { return err } req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") for k, v := range c.cfg.Headers { req.Header.Set(k, v) } if c.cfg.Secret != "" { req.Header.Set("X-Webhook-Signature", sign(c.cfg.Secret, body)) } resp, err := c.httpClient.Do(req) if err != nil { return err } defer resp.Body.Close() io.Copy(io.Discard, resp.Body) if resp.StatusCode >= 300 { return fmt.Errorf("webhook %s: unexpected status %d", url, resp.StatusCode) } return nil } func sign(secret string, body []byte) string { mac := hmac.New(sha256.New, []byte(secret)) mac.Write(body) return "sha256=" + hex.EncodeToString(mac.Sum(nil)) } func (c *Client) SendRSVP(r store.RSVP) error { guests := make([]map[string]any, len(r.Guests)) for i, g := range r.Guests { guests[i] = map[string]any{"name": g.Name, "isChild": g.IsChild} } return c.post(c.cfg.RSVPURL, "rsvp.created", map[string]any{ "guests": guests, "message": r.Message, }) } func (c *Client) SendPhotoUpload(u store.PhotoUpload, files []store.PhotoFile) error { urls := make([]string, len(files)) for i, f := range files { urls[i] = f.URL } return c.post(c.cfg.PhotoURL, "photos.uploaded", map[string]any{ "name": u.Name, "message": u.Message, "email": u.Email, "phone": u.Phone, "photoUrls": urls, }) }