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  }