feat(webhook): add webhook client for RSVP and photo uploads
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user