github.com/whoyao/protocol@v0.0.0-20230519045905-2d8ace718ca5/webhook/url_notifier.go (about) 1 package webhook 2 3 import ( 4 "bytes" 5 "crypto/sha256" 6 "encoding/base64" 7 "sync" 8 "time" 9 10 "github.com/frostbyte73/core" 11 "github.com/hashicorp/go-retryablehttp" 12 "go.uber.org/atomic" 13 "google.golang.org/protobuf/encoding/protojson" 14 15 "github.com/whoyao/protocol/auth" 16 "github.com/whoyao/protocol/livekit" 17 "github.com/whoyao/protocol/logger" 18 ) 19 20 type URLNotifierParams struct { 21 Logger logger.Logger 22 QueueSize int 23 URL string 24 APIKey string 25 APISecret string 26 } 27 28 const defaultQueueSize = 100 29 30 // URLNotifier is a QueuedNotifier that sends a POST request to a Webhook URL. 31 // It will retry on failure, and will drop events if notification fall too far behind 32 type URLNotifier struct { 33 mu sync.RWMutex 34 params URLNotifierParams 35 client *retryablehttp.Client 36 dropped atomic.Int32 37 worker core.QueueWorker 38 } 39 40 func NewURLNotifier(params URLNotifierParams) *URLNotifier { 41 if params.QueueSize == 0 { 42 params.QueueSize = defaultQueueSize 43 } 44 if params.Logger == nil { 45 params.Logger = logger.GetLogger() 46 } 47 48 n := &URLNotifier{ 49 params: params, 50 client: retryablehttp.NewClient(), 51 } 52 n.worker = core.NewQueueWorker(core.QueueWorkerParams{ 53 QueueSize: params.QueueSize, 54 DropWhenFull: true, 55 OnDropped: func() { n.dropped.Inc() }, 56 }) 57 return n 58 } 59 60 func (n *URLNotifier) SetKeys(apiKey, apiSecret string) { 61 n.mu.Lock() 62 defer n.mu.Unlock() 63 n.params.APIKey = apiKey 64 n.params.APISecret = apiSecret 65 } 66 67 func (n *URLNotifier) QueueNotify(event *livekit.WebhookEvent) error { 68 n.worker.Submit(func() { 69 if err := n.send(event); err != nil { 70 n.params.Logger.Warnw("failed to send webhook", err, "url", n.params.URL, "event", event.Event) 71 n.dropped.Add(event.NumDropped + 1) 72 } else { 73 n.params.Logger.Infow("sent webhook", "url", n.params.URL, "event", event.Event) 74 } 75 }) 76 return nil 77 } 78 79 func (n *URLNotifier) Stop(force bool) { 80 if force { 81 n.worker.Kill() 82 } else { 83 n.worker.Drain() 84 } 85 } 86 87 func (n *URLNotifier) send(event *livekit.WebhookEvent) error { 88 // set dropped count 89 event.NumDropped = n.dropped.Swap(0) 90 encoded, err := protojson.Marshal(event) 91 if err != nil { 92 return err 93 } 94 // sign payload 95 sum := sha256.Sum256(encoded) 96 b64 := base64.StdEncoding.EncodeToString(sum[:]) 97 98 n.mu.RLock() 99 apiKey := n.params.APIKey 100 apiSecret := n.params.APISecret 101 n.mu.RUnlock() 102 103 at := auth.NewAccessToken(apiKey, apiSecret). 104 SetValidFor(5 * time.Minute). 105 SetSha256(b64) 106 token, err := at.ToJWT() 107 if err != nil { 108 return err 109 } 110 r, err := retryablehttp.NewRequest("POST", n.params.URL, bytes.NewReader(encoded)) 111 if err != nil { 112 // ignore and continue 113 return err 114 } 115 r.Header.Set(authHeader, token) 116 // use a custom mime type to ensure signature is checked prior to parsing 117 r.Header.Set("content-type", "application/webhook+json") 118 res, err := n.client.Do(r) 119 if err != nil { 120 return err 121 } 122 _ = res.Body.Close() 123 return nil 124 }